CMS 3D CMS Logo

CMSSW_4_4_3_patch1/src/Alignment/MuonAlignment/python/svgfig.py

Go to the documentation of this file.
00001 # svgfig.py copyright (C) 2008 Jim Pivarski <jpivarski@gmail.com>
00002 # 
00003 # This program is free software; you can redistribute it and/or
00004 # modify it under the terms of the GNU General Public License
00005 # as published by the Free Software Foundation; either version 2
00006 # of the License, or (at your option) any later version.
00007 # 
00008 # This program is distributed in the hope that it will be useful,
00009 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00010 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00011 # GNU General Public License for more details.
00012 # 
00013 # You should have received a copy of the GNU General Public License
00014 # along with this program; if not, write to the Free Software
00015 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
00016 # 
00017 # Full licence is in the file COPYING and at https://www.gnu.org/copyleft/gpl.html
00018 
00019 import re, codecs, os, platform, copy, itertools, math, cmath, random, sys, copy
00020 _epsilon = 1e-5
00021 
00022 
00023 if re.search("windows", platform.system(), re.I):
00024   try:
00025     import _winreg
00026     _default_directory = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER, \
00027                        r"Software\Microsoft\Windows\Current Version\Explorer\Shell Folders"), "Desktop")[0]
00028 #   tmpdir = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Environment"), "TEMP")[0]
00029 #   if tmpdir[0:13] != "%USERPROFILE%":
00030 #     tmpdir = os.path.expanduser("~") + tmpdir[13:]
00031   except:
00032     _default_directory = os.path.expanduser("~") + os.sep + "Desktop"
00033 
00034 _default_fileName = "tmp.svg"
00035 
00036 _hacks = {}
00037 _hacks["inkscape-text-vertical-shift"] = False
00038 
00039 def rgb(r, g, b, maximum=1.):
00040   """Create an SVG color string "#xxyyzz" from r, g, and b.
00041 
00042   r,g,b = 0 is black and r,g,b = maximum is white.
00043   """
00044   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)))
00045 
00046 def attr_preprocess(attr):
00047   for name in attr.keys():
00048     name_colon = re.sub("__", ":", name)
00049     if name_colon != name:
00050       attr[name_colon] = attr[name]
00051       del attr[name]
00052       name = name_colon
00053 
00054     name_dash = re.sub("_", "-", name)
00055     if name_dash != name:
00056       attr[name_dash] = attr[name]
00057       del attr[name]
00058       name = name_dash
00059 
00060   return attr
00061 
00062 class SVG:
00063   """A tree representation of an SVG image or image fragment.
00064 
00065   SVG(t, sub, sub, sub..., attribute=value)
00066 
00067   t                       required             SVG type name
00068   sub                     optional list        nested SVG elements or text/Unicode
00069   attribute=value pairs   optional keywords    SVG attributes
00070 
00071   In attribute names, "__" becomes ":" and "_" becomes "-".
00072 
00073   SVG in XML
00074 
00075   <g id="mygroup" fill="blue">
00076       <rect x="1" y="1" width="2" height="2" />
00077       <rect x="3" y="3" width="2" height="2" />
00078   </g>
00079 
00080   SVG in Python
00081 
00082   >>> svg = SVG("g", SVG("rect", x=1, y=1, width=2, height=2), \ 
00083   ...                SVG("rect", x=3, y=3, width=2, height=2), \ 
00084   ...           id="mygroup", fill="blue")
00085 
00086   Sub-elements and attributes may be accessed through tree-indexing:
00087 
00088   >>> svg = SVG("text", SVG("tspan", "hello there"), stroke="none", fill="black")
00089   >>> svg[0]
00090   <tspan (1 sub) />
00091   >>> svg[0, 0]
00092   'hello there'
00093   >>> svg["fill"]
00094   'black'
00095 
00096   Iteration is depth-first:
00097 
00098   >>> svg = SVG("g", SVG("g", SVG("line", x1=0, y1=0, x2=1, y2=1)), \
00099   ...                SVG("text", SVG("tspan", "hello again")))
00100   ... 
00101   >>> for ti, s in svg:
00102   ...     print ti, repr(s)
00103   ... 
00104   (0,) <g (1 sub) />
00105   (0, 0) <line x2=1 y1=0 x1=0 y2=1 />
00106   (0, 0, 'x2') 1
00107   (0, 0, 'y1') 0
00108   (0, 0, 'x1') 0
00109   (0, 0, 'y2') 1
00110   (1,) <text (1 sub) />
00111   (1, 0) <tspan (1 sub) />
00112   (1, 0, 0) 'hello again'
00113 
00114   Use "print" to navigate:
00115 
00116   >>> print svg
00117   None                 <g (2 sub) />
00118   [0]                      <g (1 sub) />
00119   [0, 0]                       <line x2=1 y1=0 x1=0 y2=1 />
00120   [1]                      <text (1 sub) />
00121   [1, 0]                       <tspan (1 sub) />
00122   """
00123   def __init__(self, *t_sub, **attr):
00124     if len(t_sub) == 0: raise TypeError, "SVG element must have a t (SVG type)"
00125 
00126     # first argument is t (SVG type)
00127     self.t = t_sub[0]
00128     # the rest are sub-elements
00129     self.sub = list(t_sub[1:])
00130     
00131     # keyword arguments are attributes
00132     # need to preprocess to handle differences between SVG and Python syntax
00133     self.attr = attr_preprocess(attr)
00134 
00135   def __getitem__(self, ti):
00136     """Index is a list that descends tree, returning a sub-element if
00137     it ends with a number and an attribute if it ends with a string."""
00138     obj = self
00139     if isinstance(ti, (list, tuple)):
00140       for i in ti[:-1]: obj = obj[i]
00141       ti = ti[-1]
00142 
00143     if isinstance(ti, (int, long, slice)): return obj.sub[ti]
00144     else: return obj.attr[ti]
00145 
00146   def __setitem__(self, ti, value):
00147     """Index is a list that descends tree, returning a sub-element if
00148     it ends with a number and an attribute if it ends with a string."""
00149     obj = self
00150     if isinstance(ti, (list, tuple)):
00151       for i in ti[:-1]: obj = obj[i]
00152       ti = ti[-1]
00153 
00154     if isinstance(ti, (int, long, slice)): obj.sub[ti] = value
00155     else: obj.attr[ti] = value
00156 
00157   def __delitem__(self, ti):
00158     """Index is a list that descends tree, returning a sub-element if
00159     it ends with a number and an attribute if it ends with a string."""
00160     obj = self
00161     if isinstance(ti, (list, tuple)):
00162       for i in ti[:-1]: obj = obj[i]
00163       ti = ti[-1]
00164 
00165     if isinstance(ti, (int, long, slice)): del obj.sub[ti]
00166     else: del obj.attr[ti]
00167 
00168   def __contains__(self, value):
00169     """x in svg == True iff x is an attribute in svg."""
00170     return value in self.attr
00171 
00172   def __eq__(self, other):
00173     """x == y iff x represents the same SVG as y."""
00174     if id(self) == id(other): return True
00175     return isinstance(other, SVG) and self.t == other.t and self.sub == other.sub and self.attr == other.attr
00176 
00177   def __ne__(self, other):
00178     """x != y iff x does not represent the same SVG as y."""
00179     return not (self == other)
00180 
00181   def append(self, x):
00182     """Appends x to the list of sub-elements (drawn last, overlaps
00183     other primatives)."""
00184     self.sub.append(x)
00185 
00186   def prepend(self, x):
00187     """Prepends x to the list of sub-elements (drawn first may be
00188     overlapped by other primatives)."""
00189     self.sub[0:0] = [x]
00190 
00191   def extend(self, x):
00192     """Extends list of sub-elements by a list x."""
00193     self.sub.extend(x)
00194 
00195   def clone(self, shallow=False):
00196     """Deep copy of SVG tree.  Set shallow=True for a shallow copy."""
00197     if shallow:
00198       return copy.copy(self)
00199     else:
00200       return copy.deepcopy(self)
00201 
00202   ### nested class
00203   class SVGDepthIterator:
00204     """Manages SVG iteration."""
00205 
00206     def __init__(self, svg, ti, depth_limit):
00207       self.svg = svg
00208       self.ti = ti
00209       self.shown = False
00210       self.depth_limit = depth_limit
00211 
00212     def __iter__(self): return self
00213 
00214     def next(self):
00215       if not self.shown:
00216         self.shown = True
00217         if self.ti != ():
00218           return self.ti, self.svg
00219 
00220       if not isinstance(self.svg, SVG): raise StopIteration
00221       if self.depth_limit != None and len(self.ti) >= self.depth_limit: raise StopIteration
00222 
00223       if "iterators" not in self.__dict__:
00224         self.iterators = []
00225         for i, s in enumerate(self.svg.sub):
00226           self.iterators.append(self.__class__(s, self.ti + (i,), self.depth_limit))
00227         for k, s in self.svg.attr.items():
00228           self.iterators.append(self.__class__(s, self.ti + (k,), self.depth_limit))
00229         self.iterators = itertools.chain(*self.iterators)
00230 
00231       return self.iterators.next()
00232   ### end nested class
00233 
00234   def depth_first(self, depth_limit=None):
00235     """Returns a depth-first generator over the SVG.  If depth_limit
00236     is a number, stop recursion at that depth."""
00237     return self.SVGDepthIterator(self, (), depth_limit)
00238 
00239   def breadth_first(self, depth_limit=None):
00240     """Not implemented yet.  Any ideas on how to do it?
00241 
00242     Returns a breadth-first generator over the SVG.  If depth_limit
00243     is a number, stop recursion at that depth."""
00244     raise NotImplementedError, "Got an algorithm for breadth-first searching a tree without effectively copying the tree?"
00245 
00246   def __iter__(self): return self.depth_first()
00247 
00248   def items(self, sub=True, attr=True, text=True):
00249     """Get a recursively-generated list of tree-index, sub-element/attribute pairs.
00250 
00251     If sub == False, do not show sub-elements.
00252     If attr == False, do not show attributes.
00253     If text == False, do not show text/Unicode sub-elements.
00254     """
00255     output = []
00256     for ti, s in self:
00257       show = False
00258       if isinstance(ti[-1], (int, long)):
00259         if isinstance(s, basestring): show = text
00260         else: show = sub
00261       else: show = attr
00262 
00263       if show: output.append((ti, s))
00264     return output
00265 
00266   def keys(self, sub=True, attr=True, text=True):
00267     """Get a recursively-generated list of tree-indexes.
00268 
00269     If sub == False, do not show sub-elements.
00270     If attr == False, do not show attributes.
00271     If text == False, do not show text/Unicode sub-elements.
00272     """
00273     return [ti for ti, s in self.items(sub, attr, text)]
00274 
00275   def values(self, sub=True, attr=True, text=True):
00276     """Get a recursively-generated list of sub-elements and attributes.
00277 
00278     If sub == False, do not show sub-elements.
00279     If attr == False, do not show attributes.
00280     If text == False, do not show text/Unicode sub-elements.
00281     """
00282     return [s for ti, s in self.items(sub, attr, text)]
00283 
00284   def __repr__(self): return self.xml(depth_limit=0)
00285 
00286   def __str__(self):
00287     """Print (actually, return a string of) the tree in a form useful for browsing."""
00288     return self.tree(sub=True, attr=False, text=False)
00289 
00290   def tree(self, depth_limit=None, sub=True, attr=True, text=True, tree_width=20, obj_width=80):
00291     """Print (actually, return a string of) the tree in a form useful for browsing.
00292 
00293     If depth_limit == a number, stop recursion at that depth.
00294     If sub == False, do not show sub-elements.
00295     If attr == False, do not show attributes.
00296     If text == False, do not show text/Unicode sub-elements.
00297     tree_width is the number of characters reserved for printing tree indexes.
00298     obj_width is the number of characters reserved for printing sub-elements/attributes.
00299     """
00300 
00301     output = []
00302 
00303     line = "%s %s" % (("%%-%ds" % tree_width) % repr(None), ("%%-%ds" % obj_width) % (repr(self))[0:obj_width])
00304     output.append(line)
00305 
00306     for ti, s in self.depth_first(depth_limit):
00307       show = False
00308       if isinstance(ti[-1], (int, long)):
00309         if isinstance(s, basestring): show = text
00310         else: show = sub
00311       else: show = attr
00312 
00313       if show:
00314         line = "%s %s" % (("%%-%ds" % tree_width) % repr(list(ti)), ("%%-%ds" % obj_width) % ("    "*len(ti) + repr(s))[0:obj_width])
00315         output.append(line)
00316 
00317     return "\n".join(output)
00318 
00319   def xml(self, indent="    ", newl="\n", depth_limit=None, depth=0):
00320     """Get an XML representation of the SVG.
00321 
00322     indent      string used for indenting
00323     newl        string used for newlines
00324     If depth_limit == a number, stop recursion at that depth.
00325     depth       starting depth (not useful for users)
00326 
00327     print svg.xml()
00328     """
00329 
00330     attrstr = []
00331     for n, v in self.attr.items():
00332       if isinstance(v, dict):
00333         v = "; ".join(["%s:%s" % (ni, vi) for ni, vi in v.items()])
00334       elif isinstance(v, (list, tuple)):
00335         v = ", ".join(v)
00336       attrstr.append(" %s=%s" % (n, repr(v)))
00337     attrstr = "".join(attrstr)
00338 
00339     if len(self.sub) == 0: return "%s<%s%s />" % (indent * depth, self.t, attrstr)
00340 
00341     if depth_limit == None or depth_limit > depth:
00342       substr = []
00343       for s in self.sub:
00344         if isinstance(s, SVG):
00345           substr.append(s.xml(indent, newl, depth_limit, depth + 1) + newl)
00346         elif isinstance(s, str):
00347           substr.append("%s%s%s" % (indent * (depth + 1), s, newl))
00348         else:
00349           substr.append("%s%s%s" % (indent * (depth + 1), repr(s), newl))
00350       substr = "".join(substr)
00351 
00352       return "%s<%s%s>%s%s%s</%s>" % (indent * depth, self.t, attrstr, newl, substr, indent * depth, self.t)
00353 
00354     else:
00355       return "%s<%s (%d sub)%s />" % (indent * depth, self.t, len(self.sub), attrstr)
00356 
00357   def standalone_xml(self, indent="    ", newl="\n"):
00358     """Get an XML representation of the SVG that can be saved/rendered.
00359 
00360     indent      string used for indenting
00361     newl        string used for newlines
00362     """
00363 
00364     if self.t == "svg": top = self
00365     else: top = canvas(self)
00366     return """\
00367 <?xml version="1.0" standalone="no"?>
00368 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "https://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
00369 
00370 """ + ("".join(top.__standalone_xml(indent, newl)))  # end of return statement
00371 
00372   def __standalone_xml(self, indent, newl):
00373     output = [u"<%s" % self.t]
00374 
00375     for n, v in self.attr.items():
00376       if isinstance(v, dict):
00377         v = "; ".join(["%s:%s" % (ni, vi) for ni, vi in v.items()])
00378       elif isinstance(v, (list, tuple)):
00379         v = ", ".join(v)
00380       output.append(u" %s=\"%s\"" % (n, v))
00381 
00382     if len(self.sub) == 0:
00383       output.append(u" />%s%s" % (newl, newl))
00384       return output
00385 
00386     elif self.t == "text" or self.t == "tspan" or self.t == "style":
00387       output.append(u">")
00388 
00389     else:
00390       output.append(u">%s%s" % (newl, newl))
00391 
00392     for s in self.sub:
00393       if isinstance(s, SVG): output.extend(s.__standalone_xml(indent, newl))
00394       else: output.append(unicode(s))
00395 
00396     if self.t == "tspan": output.append(u"</%s>" % self.t)
00397     else: output.append(u"</%s>%s%s" % (self.t, newl, newl))
00398 
00399     return output
00400 
00401   def interpret_fileName(self, fileName=None):
00402     if fileName == None:
00403       fileName = _default_fileName
00404     if re.search("windows", platform.system(), re.I) and not os.path.isabs(fileName):
00405       fileName = _default_directory + os.sep + fileName
00406     return fileName
00407 
00408   def save(self, fileName=None, encoding="utf-8", compresslevel=None):
00409     """Save to a file for viewing.  Note that svg.save() overwrites the file named _default_fileName.
00410 
00411     fileName        default=None            note that _default_fileName will be overwritten if
00412                                             no fileName is specified. If the extension
00413                                             is ".svgz" or ".gz", the output will be gzipped
00414     encoding        default="utf-8"       file encoding (default is Unicode)
00415     compresslevel   default=None            if a number, the output will be gzipped with that
00416                                             compression level (1-9, 1 being fastest and 9 most
00417                                             thorough)
00418     """
00419     fileName = self.interpret_fileName(fileName)
00420 
00421     if compresslevel != None or re.search("\.svgz$", fileName, re.I) or re.search("\.gz$", fileName, re.I):
00422       import gzip
00423       if compresslevel == None:
00424         f = gzip.GzipFile(fileName, "w")
00425       else:
00426         f = gzip.GzipFile(fileName, "w", compresslevel)
00427 
00428       f = codecs.EncodedFile(f, "utf-8", encoding)
00429       f.write(self.standalone_xml())
00430       f.close()
00431 
00432     else:
00433       f = codecs.open(fileName, "w", encoding=encoding)
00434       f.write(self.standalone_xml())
00435       f.close()
00436 
00437   def inkview(self, fileName=None, encoding="utf-8"):
00438     """View in "inkview", assuming that program is available on your system.
00439 
00440     fileName        default=None            note that any file named _default_fileName will be
00441                                             overwritten if no fileName is specified. If the extension
00442                                             is ".svgz" or ".gz", the output will be gzipped
00443     encoding        default="utf-8"       file encoding (default is Unicode)
00444     """
00445     fileName = self.interpret_fileName(fileName)
00446     self.save(fileName, encoding)
00447     os.spawnvp(os.P_NOWAIT, "inkview", ("inkview", fileName))
00448 
00449   def inkscape(self, fileName=None, encoding="utf-8"):
00450     """View in "inkscape", assuming that program is available on your system.
00451 
00452     fileName        default=None            note that any file named _default_fileName will be
00453                                             overwritten if no fileName is specified. If the extension
00454                                             is ".svgz" or ".gz", the output will be gzipped
00455     encoding        default="utf-8"       file encoding (default is Unicode)
00456     """
00457     fileName = self.interpret_fileName(fileName)
00458     self.save(fileName, encoding)
00459     os.spawnvp(os.P_NOWAIT, "inkscape", ("inkscape", fileName))
00460 
00461   def firefox(self, fileName=None, encoding="utf-8"):
00462     """View in "firefox", assuming that program is available on your system.
00463 
00464     fileName        default=None            note that any file named _default_fileName will be
00465                                             overwritten if no fileName is specified. If the extension
00466                                             is ".svgz" or ".gz", the output will be gzipped
00467     encoding        default="utf-8"       file encoding (default is Unicode)
00468     """
00469     fileName = self.interpret_fileName(fileName)
00470     self.save(fileName, encoding)
00471     os.spawnvp(os.P_NOWAIT, "firefox", ("firefox", fileName))
00472 
00473 ######################################################################
00474 
00475 _canvas_defaults = {"width": "400px", "height": "400px", "viewBox": "0 0 100 100", \
00476                    "xmlns": "https://www.w3.org/2000/svg", "xmlns:xlink": "https://www.w3.org/1999/xlink", "version":"1.1", \
00477                    "style": {"stroke":"black", "fill":"none", "stroke-width":"0.5pt", "stroke-linejoin":"round", "text-anchor":"middle"}, \
00478                    "font-family": ["Helvetica", "Arial", "FreeSans", "Sans", "sans", "sans-serif"], \
00479                    }
00480 
00481 def canvas(*sub, **attr):
00482   """Creates a top-level SVG object, allowing the user to control the
00483   image size and aspect ratio.
00484 
00485   canvas(sub, sub, sub..., attribute=value)
00486 
00487   sub                     optional list       nested SVG elements or text/Unicode
00488   attribute=value pairs   optional keywords   SVG attributes
00489 
00490   Default attribute values:
00491 
00492   width           "400px"
00493   height          "400px"
00494   viewBox         "0 0 100 100"
00495   xmlns           "https://www.w3.org/2000/svg"
00496   xmlns:xlink     "https://www.w3.org/1999/xlink"
00497   version         "1.1"
00498   style           "stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoin:round; text-anchor:middle"
00499   font-family     "Helvetica,Arial,FreeSans?,Sans,sans,sans-serif"
00500   """
00501   attributes = dict(_canvas_defaults)
00502   attributes.update(attr)
00503 
00504   if sub == None or sub == ():
00505     return SVG("svg", **attributes)
00506   else:
00507     return SVG("svg", *sub, **attributes)
00508 
00509 def canvas_outline(*sub, **attr):
00510   """Same as canvas(), but draws an outline around the drawable area,
00511   so that you know how close your image is to the edges."""
00512   svg = canvas(*sub, **attr)
00513   match = re.match("[, \t]*([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]*", svg["viewBox"])
00514   if match == None: raise ValueError, "canvas viewBox is incorrectly formatted"
00515   x, y, width, height = [float(x) for x in match.groups()]
00516   svg.prepend(SVG("rect", x=x, y=y, width=width, height=height, stroke="none", fill="cornsilk"))
00517   svg.append(SVG("rect", x=x, y=y, width=width, height=height, stroke="black", fill="none"))
00518   return svg
00519 
00520 def template(fileName, svg, replaceme="REPLACEME"):
00521   """Loads an SVG image from a file, replacing instances of
00522   <REPLACEME /> with a given svg object.
00523 
00524   fileName         required                name of the template SVG
00525   svg              required                SVG object for replacement
00526   replaceme        default="REPLACEME"   fake SVG element to be replaced by the given object
00527 
00528   >>> print load("template.svg")
00529   None                 <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
00530   [0]                      <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
00531   [1]                      <REPLACEME />
00532   >>> 
00533   >>> print template("template.svg", SVG("circle", cx=50, cy=50, r=30))
00534   None                 <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
00535   [0]                      <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
00536   [1]                      <circle cy=50 cx=50 r=30 />
00537   """
00538   output = load(fileName)
00539   for ti, s in output:
00540     if isinstance(s, SVG) and s.t == replaceme:
00541       output[ti] = svg
00542   return output
00543 
00544 ######################################################################
00545 
00546 def load(fileName):
00547   """Loads an SVG image from a file."""
00548   return load_stream(file(fileName))
00549 
00550 def load_stream(stream):
00551   """Loads an SVG image from a stream (can be a string or a file object)."""
00552 
00553   from xml.sax import handler, make_parser
00554   from xml.sax.handler import feature_namespaces, feature_external_ges, feature_external_pes
00555 
00556   class ContentHandler(handler.ContentHandler):
00557     def __init__(self):
00558       self.stack = []
00559       self.output = None
00560       self.all_whitespace = re.compile("^\s*$")
00561 
00562     def startElement(self, name, attr):
00563       s = SVG(name)
00564       s.attr = dict(attr.items())
00565       if len(self.stack) > 0:
00566         last = self.stack[-1]
00567         last.sub.append(s)
00568       self.stack.append(s)
00569 
00570     def characters(self, ch):
00571       if not isinstance(ch, basestring) or self.all_whitespace.match(ch) == None:
00572         if len(self.stack) > 0:
00573           last = self.stack[-1]
00574           if len(last.sub) > 0 and isinstance(last.sub[-1], basestring):
00575             last.sub[-1] = last.sub[-1] + "\n" + ch
00576           else:
00577             last.sub.append(ch)
00578 
00579     def endElement(self, name):
00580       if len(self.stack) > 0:
00581         last = self.stack[-1]
00582         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], basestring):
00583           last.sub[0] = "<![CDATA[\n" + last.sub[0] + "]]>"
00584 
00585       self.output = self.stack.pop()
00586 
00587   ch = ContentHandler()
00588   parser = make_parser()
00589   parser.setContentHandler(ch)
00590   parser.setFeature(feature_namespaces, 0)
00591   parser.setFeature(feature_external_ges, 0)
00592   parser.parse(stream)
00593   return ch.output
00594 
00595 ######################################################################
00596 
00597 def totrans(expr, vars=("x", "y"), globals=None, locals=None):
00598   """Converts to a coordinate transformation (a function that accepts
00599   two arguments and returns two values).
00600 
00601   expr       required                  a string expression or a function
00602                                        of two real or one complex value
00603   vars       default=("x", "y")    independent variable names;
00604                                        a singleton ("z",) is interpreted
00605                                        as complex
00606   globals    default=None              dict of global variables
00607   locals     default=None              dict of local variables
00608   """
00609 
00610   if callable(expr):
00611     if expr.func_code.co_argcount == 2:
00612       return expr
00613 
00614     elif expr.func_code.co_argcount == 1:
00615       split = lambda z: (z.real, z.imag)
00616       output = lambda x, y: split(expr(x + y*1j))
00617       output.func_name = expr.func_name
00618       return output
00619 
00620     else:
00621       raise TypeError, "must be a function of 2 or 1 variables"
00622 
00623   if len(vars) == 2:
00624     g = math.__dict__
00625     if globals != None: g.update(globals)
00626     output = eval("lambda %s, %s: (%s)" % (vars[0], vars[1], expr), g, locals)
00627     output.func_name = "%s,%s -> %s" % (vars[0], vars[1], expr)
00628     return output
00629 
00630   elif len(vars) == 1:
00631     g = cmath.__dict__
00632     if globals != None: g.update(globals)
00633     output = eval("lambda %s: (%s)" % (vars[0], expr), g, locals)
00634     split = lambda z: (z.real, z.imag)
00635     output2 = lambda x, y: split(output(x + y*1j))
00636     output2.func_name = "%s -> %s" % (vars[0], expr)
00637     return output2
00638 
00639   else:
00640     raise TypeError, "vars must have 2 or 1 elements"
00641 
00642 def window(xmin, xmax, ymin, ymax, x=0, y=0, width=100, height=100, xlogbase=None, ylogbase=None, minusInfinity=-1000, flipx=False, flipy=True):
00643   """Creates and returns a coordinate transformation (a function that
00644   accepts two arguments and returns two values) that transforms from
00645       (xmin, ymin), (xmax, ymax)
00646   to
00647       (x, y), (x + width, y + height).
00648 
00649   xlogbase, ylogbase    default=None, None     if a number, transform
00650                                                logarithmically with given base
00651   minusInfinity         default=-1000          what to return if
00652                                                log(0 or negative) is attempted
00653   flipx                 default=False          if true, reverse the direction of x
00654   flipy                 default=True           if true, reverse the direction of y
00655 
00656   (When composing windows, be sure to set flipy=False.)
00657   """
00658 
00659   if flipx:
00660     ox1 = x + width
00661     ox2 = x
00662   else:
00663     ox1 = x
00664     ox2 = x + width
00665   if flipy:
00666     oy1 = y + height
00667     oy2 = y
00668   else:
00669     oy1 = y
00670     oy2 = y + height
00671   ix1 = xmin
00672   iy1 = ymin
00673   ix2 = xmax
00674   iy2 = ymax
00675   
00676   if xlogbase != None and (ix1 <= 0. or ix2 <= 0.): raise ValueError, "x range incompatible with log scaling: (%g, %g)" % (ix1, ix2)
00677 
00678   if ylogbase != None and (iy1 <= 0. or iy2 <= 0.): raise ValueError, "y range incompatible with log scaling: (%g, %g)" % (iy1, iy2)
00679 
00680   def maybelog(t, it1, it2, ot1, ot2, logbase):
00681     if t <= 0.: return minusInfinity
00682     else:
00683       return ot1 + 1.*(math.log(t, logbase) - math.log(it1, logbase))/(math.log(it2, logbase) - math.log(it1, logbase)) * (ot2 - ot1)
00684 
00685   xlogstr, ylogstr = "", ""
00686 
00687   if xlogbase == None:
00688     xfunc = lambda x: ox1 + 1.*(x - ix1)/(ix2 - ix1) * (ox2 - ox1)
00689   else:
00690     xfunc = lambda x: maybelog(x, ix1, ix2, ox1, ox2, xlogbase)
00691     xlogstr = " xlog=%g" % xlogbase
00692 
00693   if ylogbase == None:
00694     yfunc = lambda y: oy1 + 1.*(y - iy1)/(iy2 - iy1) * (oy2 - oy1)
00695   else:
00696     yfunc = lambda y: maybelog(y, iy1, iy2, oy1, oy2, ylogbase)
00697     ylogstr = " ylog=%g" % ylogbase
00698 
00699   output = lambda x, y: (xfunc(x), yfunc(y))
00700 
00701   output.func_name = "(%g, %g), (%g, %g) -> (%g, %g), (%g, %g)%s%s" % (ix1, ix2, iy1, iy2, ox1, ox2, oy1, oy2, xlogstr, ylogstr)
00702   return output
00703 
00704 def rotate(angle, cx=0, cy=0):
00705   """Creates and returns a coordinate transformation which rotates
00706   around (cx,cy) by "angle" degrees."""
00707   angle *= math.pi/180.
00708   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))
00709 
00710 class Fig:
00711   """Stores graphics primitive objects and applies a single coordinate
00712   transformation to them. To compose coordinate systems, nest Fig
00713   objects.
00714 
00715   Fig(obj, obj, obj..., trans=function)
00716 
00717   obj     optional list    a list of drawing primatives
00718   trans   default=None     a coordinate transformation function
00719 
00720   >>> fig = Fig(Line(0,0,1,1), Rect(0.2,0.2,0.8,0.8), trans="2*x, 2*y")
00721   >>> print fig.SVG().xml()
00722   <g>
00723       <path d='M0 0L2 2' />
00724       <path d='M0.4 0.4L1.6 0.4ZL1.6 1.6ZL0.4 1.6ZL0.4 0.4ZZ' />
00725   </g>
00726   >>> print Fig(fig, trans="x/2., y/2.").SVG().xml()
00727   <g>
00728       <path d='M0 0L1 1' />
00729       <path d='M0.2 0.2L0.8 0.2ZL0.8 0.8ZL0.2 0.8ZL0.2 0.2ZZ' />
00730   </g>
00731   """
00732 
00733   def __repr__(self):
00734     if self.trans == None:
00735       return "<Fig (%d items)>" % len(self.d)
00736     elif isinstance(self.trans, basestring):
00737       return "<Fig (%d items) x,y -> %s>" % (len(self.d), self.trans)
00738     else:
00739       return "<Fig (%d items) %s>" % (len(self.d), self.trans.func_name)
00740 
00741   def __init__(self, *d, **kwds):
00742     self.d = list(d)
00743     defaults = {"trans":None}
00744     defaults.update(kwds)
00745     kwds = defaults
00746 
00747     self.trans = kwds["trans"]; del kwds["trans"]
00748     if len(kwds) != 0:
00749       raise TypeError, "Fig() got unexpected keyword arguments %s" % kwds.keys()
00750 
00751   def SVG(self, trans=None):
00752     """Apply the transformation "trans" and return an SVG object.
00753 
00754     Coordinate transformations in nested Figs will be composed.
00755     """
00756 
00757     if trans == None: trans = self.trans
00758     if isinstance(trans, basestring): trans = totrans(trans)
00759 
00760     output = SVG("g")
00761     for s in self.d:
00762       if isinstance(s, SVG):
00763         output.append(s)
00764 
00765       elif isinstance(s, Fig):
00766         strans = s.trans
00767         if isinstance(strans, basestring): strans = totrans(strans)
00768 
00769         if trans == None: subtrans = strans
00770         elif strans == None: subtrans = trans
00771         else: subtrans = lambda x,y: trans(*strans(x, y))
00772 
00773         output.sub += s.SVG(subtrans).sub
00774 
00775       elif s == None: pass
00776 
00777       else:
00778         output.append(s.SVG(trans))
00779 
00780     return output
00781 
00782 class Plot:
00783   """Acts like Fig, but draws a coordinate axis. You also need to supply plot ranges.
00784 
00785   Plot(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
00786 
00787   xmin, xmax      required        minimum and maximum x values (in the objs' coordinates)
00788   ymin, ymax      required        minimum and maximum y values (in the objs' coordinates)
00789   obj             optional list   drawing primatives
00790   keyword options keyword list    options defined below
00791 
00792   The following are keyword options, with their default values:
00793 
00794   trans           None          transformation function
00795   x, y            5, 5          upper-left corner of the Plot in SVG coordinates
00796   width, height   90, 90        width and height of the Plot in SVG coordinates
00797   flipx, flipy    False, True   flip the sign of the coordinate axis
00798   minusInfinity   -1000         if an axis is logarithmic and an object is plotted at 0 or
00799                                 a negative value, -1000 will be used as a stand-in for NaN
00800   atx, aty        0, 0          the place where the coordinate axes cross
00801   xticks          -10           request ticks according to the standard tick specification
00802                                 (see help(Ticks))
00803   xminiticks      True          request miniticks according to the standard minitick
00804                                 specification
00805   xlabels         True          request tick labels according to the standard tick label
00806                                 specification
00807   xlogbase        None          if a number, the axis and transformation are logarithmic
00808                                 with ticks at the given base (10 being the most common)
00809   (same for y)
00810   arrows          None          if a new identifier, create arrow markers and draw them
00811                                 at the ends of the coordinate axes
00812   text_attr       {}            a dictionary of attributes for label text
00813   axis_attr       {}            a dictionary of attributes for the axis lines
00814   """
00815 
00816   def __repr__(self):
00817     if self.trans == None:
00818       return "<Plot (%d items)>" % len(self.d)
00819     else:
00820       return "<Plot (%d items) %s>" % (len(self.d), self.trans.func_name)
00821 
00822   def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
00823     self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
00824     self.d = list(d)
00825     defaults = {"trans":None, "x":5, "y":5, "width":90, "height":90, "flipx":False, "flipy":True, "minusInfinity":-1000, \
00826                 "atx":0, "xticks":-10, "xminiticks":True, "xlabels":True, "xlogbase":None, \
00827                 "aty":0, "yticks":-10, "yminiticks":True, "ylabels":True, "ylogbase":None, \
00828                 "arrows":None, "text_attr":{}, "axis_attr":{}}
00829     defaults.update(kwds)
00830     kwds = defaults
00831 
00832     self.trans = kwds["trans"]; del kwds["trans"]
00833     self.x = kwds["x"]; del kwds["x"]
00834     self.y = kwds["y"]; del kwds["y"]
00835     self.width = kwds["width"]; del kwds["width"]
00836     self.height = kwds["height"]; del kwds["height"]
00837     self.flipx = kwds["flipx"]; del kwds["flipx"]
00838     self.flipy = kwds["flipy"]; del kwds["flipy"]
00839     self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
00840     self.atx = kwds["atx"]; del kwds["atx"]
00841     self.xticks = kwds["xticks"]; del kwds["xticks"]
00842     self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
00843     self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
00844     self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
00845     self.aty = kwds["aty"]; del kwds["aty"]
00846     self.yticks = kwds["yticks"]; del kwds["yticks"]
00847     self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
00848     self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
00849     self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
00850     self.arrows = kwds["arrows"]; del kwds["arrows"]
00851     self.text_attr = kwds["text_attr"]; del kwds["text_attr"]
00852     self.axis_attr = kwds["axis_attr"]; del kwds["axis_attr"]
00853     if len(kwds) != 0:
00854       raise TypeError, "Plot() got unexpected keyword arguments %s" % kwds.keys()
00855 
00856   def SVG(self, trans=None):
00857     """Apply the transformation "trans" and return an SVG object."""
00858     if trans == None: trans = self.trans
00859     if isinstance(trans, basestring): trans = totrans(trans)
00860 
00861     self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax, x=self.x, y=self.y, width=self.width, height=self.height, \
00862                               xlogbase=self.xlogbase, ylogbase=self.ylogbase, minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
00863 
00864     d = [Axes(self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty, \
00865               self.xticks, self.xminiticks, self.xlabels, self.xlogbase, \
00866               self.yticks, self.yminiticks, self.ylabels, self.ylogbase, \
00867               self.arrows, self.text_attr, **self.axis_attr)] \
00868         + self.d
00869 
00870     return Fig(Fig(*d, **{"trans":trans})).SVG(self.last_window)
00871     
00872 class Frame:
00873   text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
00874   axis_defaults = {}
00875 
00876   tick_length = 1.5
00877   minitick_length = 0.75
00878   text_xaxis_offset = 1.
00879   text_yaxis_offset = 2.
00880   text_xtitle_offset = 6.
00881   text_ytitle_offset = 12.
00882 
00883   def __repr__(self):
00884     return "<Frame (%d items)>" % len(self.d)
00885 
00886   def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
00887     """Acts like Fig, but draws a coordinate frame around the data. You also need to supply plot ranges.
00888 
00889     Frame(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
00890 
00891     xmin, xmax      required        minimum and maximum x values (in the objs' coordinates)
00892     ymin, ymax      required        minimum and maximum y values (in the objs' coordinates)
00893     obj             optional list   drawing primatives
00894     keyword options keyword list    options defined below
00895 
00896     The following are keyword options, with their default values:
00897 
00898     x, y            20, 5         upper-left corner of the Frame in SVG coordinates
00899     width, height   75, 80        width and height of the Frame in SVG coordinates
00900     flipx, flipy    False, True   flip the sign of the coordinate axis
00901     minusInfinity   -1000         if an axis is logarithmic and an object is plotted at 0 or
00902                                   a negative value, -1000 will be used as a stand-in for NaN
00903     xtitle          None          if a string, label the x axis
00904     xticks          -10           request ticks according to the standard tick specification
00905                                   (see help(Ticks))
00906     xminiticks      True          request miniticks according to the standard minitick
00907                                   specification
00908     xlabels         True          request tick labels according to the standard tick label
00909                                   specification
00910     xlogbase        None          if a number, the axis and transformation are logarithmic
00911                                   with ticks at the given base (10 being the most common)
00912     (same for y)
00913     text_attr       {}            a dictionary of attributes for label text
00914     axis_attr       {}            a dictionary of attributes for the axis lines
00915     """
00916 
00917     self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
00918     self.d = list(d)
00919     defaults = {"x":20, "y":5, "width":75, "height":80, "flipx":False, "flipy":True, "minusInfinity":-1000, \
00920                 "xtitle":None, "xticks":-10, "xminiticks":True, "xlabels":True, "x2labels":None, "xlogbase":None, \
00921                 "ytitle":None, "yticks":-10, "yminiticks":True, "ylabels":True, "y2labels":None, "ylogbase":None, \
00922                 "text_attr":{}, "axis_attr":{}}
00923     defaults.update(kwds)
00924     kwds = defaults
00925 
00926     self.x = kwds["x"]; del kwds["x"]
00927     self.y = kwds["y"]; del kwds["y"]
00928     self.width = kwds["width"]; del kwds["width"]
00929     self.height = kwds["height"]; del kwds["height"]
00930     self.flipx = kwds["flipx"]; del kwds["flipx"]
00931     self.flipy = kwds["flipy"]; del kwds["flipy"]
00932     self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
00933     self.xtitle = kwds["xtitle"]; del kwds["xtitle"]
00934     self.xticks = kwds["xticks"]; del kwds["xticks"]
00935     self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
00936     self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
00937     self.x2labels = kwds["x2labels"]; del kwds["x2labels"]
00938     self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
00939     self.ytitle = kwds["ytitle"]; del kwds["ytitle"]
00940     self.yticks = kwds["yticks"]; del kwds["yticks"]
00941     self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
00942     self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
00943     self.y2labels = kwds["y2labels"]; del kwds["y2labels"]
00944     self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
00945 
00946     self.text_attr = dict(self.text_defaults)
00947     self.text_attr.update(kwds["text_attr"]); del kwds["text_attr"]
00948 
00949     self.axis_attr = dict(self.axis_defaults)
00950     self.axis_attr.update(kwds["axis_attr"]); del kwds["axis_attr"]
00951 
00952     if len(kwds) != 0:
00953       raise TypeError, "Frame() got unexpected keyword arguments %s" % kwds.keys()
00954 
00955   def SVG(self):
00956     """Apply the window transformation and return an SVG object."""
00957 
00958     self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax, x=self.x, y=self.y, width=self.width, height=self.height, \
00959                               xlogbase=self.xlogbase, ylogbase=self.ylogbase, minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
00960 
00961     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)
00962     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)
00963     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)
00964     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)
00965 
00966     left.tick_start = -self.tick_length
00967     left.tick_end = 0
00968     left.minitick_start = -self.minitick_length
00969     left.minitick_end = 0.
00970     left.text_start = self.text_yaxis_offset
00971 
00972     right.tick_start = 0.
00973     right.tick_end = self.tick_length
00974     right.minitick_start = 0.
00975     right.minitick_end = self.minitick_length
00976     right.text_start = -self.text_yaxis_offset
00977     right.text_attr["text-anchor"] = "start"
00978 
00979     bottom.tick_start = 0.
00980     bottom.tick_end = self.tick_length
00981     bottom.minitick_start = 0.
00982     bottom.minitick_end = self.minitick_length
00983     bottom.text_start = -self.text_xaxis_offset
00984 
00985     top.tick_start = -self.tick_length
00986     top.tick_end = 0.
00987     top.minitick_start = -self.minitick_length
00988     top.minitick_end = 0.
00989     top.text_start = self.text_xaxis_offset
00990     top.text_attr["dominant-baseline"] = "text-after-edge"
00991 
00992     output = Fig(*self.d).SVG(self.last_window)
00993     output.prepend(left.SVG(self.last_window))
00994     output.prepend(bottom.SVG(self.last_window))
00995     output.prepend(right.SVG(self.last_window))
00996     output.prepend(top.SVG(self.last_window))
00997 
00998     if self.xtitle != None:
00999       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))
01000     if self.ytitle != None:
01001       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))
01002     return output
01003     
01004 ######################################################################
01005 
01006 def pathtoPath(svg):
01007   """Converts SVG("path", d="...") into Path(d=[...])."""
01008   if not isinstance(svg, SVG) or svg.t != "path":
01009     raise TypeError, "Only SVG <path /> objects can be converted into Paths"
01010   attr = dict(svg.attr)
01011   d = attr["d"]
01012   del attr["d"]
01013   for key in attr.keys():
01014     if not isinstance(key, str):
01015       value = attr[key]
01016       del attr[key]
01017       attr[str(key)] = value
01018   return Path(d, **attr)
01019 
01020 class Path:
01021   """Path represents an SVG path, an arbitrary set of curves and
01022   straight segments. Unlike SVG("path", d="..."), Path stores
01023   coordinates as a list of numbers, rather than a string, so that it is
01024   transformable in a Fig.
01025 
01026   Path(d, attribute=value)
01027 
01028   d                       required        path data
01029   attribute=value pairs   keyword list    SVG attributes
01030 
01031   See https://www.w3.org/TR/SVG/paths.html for specification of paths
01032   from text.
01033 
01034   Internally, Path data is a list of tuples with these definitions:
01035 
01036       * ("Z/z",): close the current path
01037       * ("H/h", x) or ("V/v", y): a horizontal or vertical line
01038         segment to x or y
01039       * ("M/m/L/l/T/t", x, y, global): moveto, lineto, or smooth
01040         quadratic curveto point (x, y). If global=True, (x, y) should
01041         not be transformed.
01042       * ("S/sQ/q", cx, cy, cglobal, x, y, global): polybezier or
01043         smooth quadratic curveto point (x, y) using (cx, cy) as a
01044         control point. If cglobal or global=True, (cx, cy) or (x, y)
01045         should not be transformed.
01046       * ("C/c", c1x, c1y, c1global, c2x, c2y, c2global, x, y, global):
01047         cubic curveto point (x, y) using (c1x, c1y) and (c2x, c2y) as
01048         control points. If c1global, c2global, or global=True, (c1x, c1y),
01049         (c2x, c2y), or (x, y) should not be transformed.
01050       * ("A/a", rx, ry, rglobal, x-axis-rotation, angle, large-arc-flag,
01051         sweep-flag, x, y, global): arcto point (x, y) using the
01052         aforementioned parameters.
01053       * (",/.", rx, ry, rglobal, angle, x, y, global): an ellipse at
01054         point (x, y) with radii (rx, ry). If angle is 0, the whole
01055         ellipse is drawn; otherwise, a partial ellipse is drawn.
01056   """
01057   defaults = {}
01058 
01059   def __repr__(self):
01060     return "<Path (%d nodes) %s>" % (len(self.d), self.attr)
01061 
01062   def __init__(self, d=[], **attr):
01063     if isinstance(d, basestring): self.d = self.parse(d)
01064     else: self.d = list(d)
01065 
01066     self.attr = dict(self.defaults)
01067     self.attr.update(attr)
01068 
01069   def parse_whitespace(self, index, pathdata):
01070     """Part of Path's text-command parsing algorithm; used internally."""
01071     while index < len(pathdata) and pathdata[index] in (" ", "\t", "\r", "\n", ","): index += 1
01072     return index, pathdata
01073 
01074   def parse_command(self, index, pathdata):
01075     """Part of Path's text-command parsing algorithm; used internally."""
01076     index, pathdata = self.parse_whitespace(index, pathdata)
01077 
01078     if index >= len(pathdata): return None, index, pathdata
01079     command = pathdata[index]
01080     if "A" <= command <= "Z" or "a" <= command <= "z":
01081       index += 1
01082       return command, index, pathdata
01083     else: 
01084       return None, index, pathdata
01085 
01086   def parse_number(self, index, pathdata):
01087     """Part of Path's text-command parsing algorithm; used internally."""
01088     index, pathdata = self.parse_whitespace(index, pathdata)
01089 
01090     if index >= len(pathdata): return None, index, pathdata
01091     first_digit = pathdata[index]
01092 
01093     if "0" <= first_digit <= "9" or first_digit in ("-", "+", "."):
01094       start = index
01095       while index < len(pathdata) and ("0" <= pathdata[index] <= "9" or pathdata[index] in ("-", "+", ".", "e", "E")):
01096         index += 1
01097       end = index
01098 
01099       index = end
01100       return float(pathdata[start:end]), index, pathdata
01101     else: 
01102       return None, index, pathdata
01103 
01104   def parse_boolean(self, index, pathdata):
01105     """Part of Path's text-command parsing algorithm; used internally."""
01106     index, pathdata = self.parse_whitespace(index, pathdata)
01107 
01108     if index >= len(pathdata): return None, index, pathdata
01109     first_digit = pathdata[index]
01110 
01111     if first_digit in ("0", "1"):
01112       index += 1
01113       return int(first_digit), index, pathdata
01114     else:
01115       return None, index, pathdata
01116 
01117   def parse(self, pathdata):
01118     """Parses text-commands, converting them into a list of tuples.
01119     Called by the constructor."""
01120     output = []
01121     index = 0
01122     while True:
01123       command, index, pathdata = self.parse_command(index, pathdata)
01124       index, pathdata = self.parse_whitespace(index, pathdata)
01125 
01126       if command == None and index == len(pathdata): break  # this is the normal way out of the loop
01127       if command in ("Z", "z"):
01128         output.append((command,))
01129 
01130       ######################
01131       elif command in ("H", "h", "V", "v"):
01132         errstring = "Path command \"%s\" requires a number at index %d" % (command, index)
01133         num1, index, pathdata = self.parse_number(index, pathdata)
01134         if num1 == None: raise ValueError, errstring
01135 
01136         while num1 != None:
01137           output.append((command, num1))
01138           num1, index, pathdata = self.parse_number(index, pathdata)
01139 
01140       ######################
01141       elif command in ("M", "m", "L", "l", "T", "t"):
01142         errstring = "Path command \"%s\" requires an x,y pair at index %d" % (command, index)
01143         num1, index, pathdata = self.parse_number(index, pathdata)
01144         num2, index, pathdata = self.parse_number(index, pathdata)
01145 
01146         if num1 == None: raise ValueError, errstring
01147 
01148         while num1 != None:
01149           if num2 == None: raise ValueError, errstring
01150           output.append((command, num1, num2, False))
01151 
01152           num1, index, pathdata = self.parse_number(index, pathdata)
01153           num2, index, pathdata = self.parse_number(index, pathdata)
01154 
01155       ######################
01156       elif command in ("S", "s", "Q", "q"):
01157         errstring = "Path command \"%s\" requires a cx,cy,x,y quadruplet at index %d" % (command, index)
01158         num1, index, pathdata = self.parse_number(index, pathdata)
01159         num2, index, pathdata = self.parse_number(index, pathdata)
01160         num3, index, pathdata = self.parse_number(index, pathdata)
01161         num4, index, pathdata = self.parse_number(index, pathdata)
01162 
01163         if num1 == None: raise ValueError, errstring
01164 
01165         while num1 != None:
01166           if num2 == None or num3 == None or num4 == None: raise ValueError, errstring
01167           output.append((command, num1, num2, False, num3, num4, False))
01168 
01169           num1, index, pathdata = self.parse_number(index, pathdata)
01170           num2, index, pathdata = self.parse_number(index, pathdata)
01171           num3, index, pathdata = self.parse_number(index, pathdata)
01172           num4, index, pathdata = self.parse_number(index, pathdata)
01173           
01174       ######################
01175       elif command in ("C", "c"):
01176         errstring = "Path command \"%s\" requires a c1x,c1y,c2x,c2y,x,y sextuplet at index %d" % (command, index)
01177         num1, index, pathdata = self.parse_number(index, pathdata)
01178         num2, index, pathdata = self.parse_number(index, pathdata)
01179         num3, index, pathdata = self.parse_number(index, pathdata)
01180         num4, index, pathdata = self.parse_number(index, pathdata)
01181         num5, index, pathdata = self.parse_number(index, pathdata)
01182         num6, index, pathdata = self.parse_number(index, pathdata)
01183         
01184         if num1 == None: raise ValueError, errstring
01185 
01186         while num1 != None:
01187           if num2 == None or num3 == None or num4 == None or num5 == None or num6 == None: raise ValueError, errstring
01188 
01189           output.append((command, num1, num2, False, num3, num4, False, num5, num6, False))
01190 
01191           num1, index, pathdata = self.parse_number(index, pathdata)
01192           num2, index, pathdata = self.parse_number(index, pathdata)
01193           num3, index, pathdata = self.parse_number(index, pathdata)
01194           num4, index, pathdata = self.parse_number(index, pathdata)
01195           num5, index, pathdata = self.parse_number(index, pathdata)
01196           num6, index, pathdata = self.parse_number(index, pathdata)
01197 
01198       ######################
01199       elif command in ("A", "a"):
01200         errstring = "Path command \"%s\" requires a rx,ry,angle,large-arc-flag,sweep-flag,x,y septuplet at index %d" % (command, index)
01201         num1, index, pathdata = self.parse_number(index, pathdata)
01202         num2, index, pathdata = self.parse_number(index, pathdata)
01203         num3, index, pathdata = self.parse_number(index, pathdata)
01204         num4, index, pathdata = self.parse_boolean(index, pathdata)
01205         num5, index, pathdata = self.parse_boolean(index, pathdata)
01206         num6, index, pathdata = self.parse_number(index, pathdata)
01207         num7, index, pathdata = self.parse_number(index, pathdata)
01208 
01209         if num1 == None: raise ValueError, errstring
01210 
01211         while num1 != None:
01212           if num2 == None or num3 == None or num4 == None or num5 == None or num6 == None or num7 == None: raise ValueError, errstring
01213 
01214           output.append((command, num1, num2, False, num3, num4, num5, num6, num7, False))
01215 
01216           num1, index, pathdata = self.parse_number(index, pathdata)
01217           num2, index, pathdata = self.parse_number(index, pathdata)
01218           num3, index, pathdata = self.parse_number(index, pathdata)
01219           num4, index, pathdata = self.parse_boolean(index, pathdata)
01220           num5, index, pathdata = self.parse_boolean(index, pathdata)
01221           num6, index, pathdata = self.parse_number(index, pathdata)
01222           num7, index, pathdata = self.parse_number(index, pathdata)
01223 
01224     return output
01225 
01226   def SVG(self, trans=None):
01227     """Apply the transformation "trans" and return an SVG object."""
01228     if isinstance(trans, basestring): trans = totrans(trans)
01229 
01230     x, y, X, Y = None, None, None, None
01231     output = []
01232     for datum in self.d:
01233       if not isinstance(datum, (tuple, list)):
01234         raise TypeError, "pathdata elements must be tuples/lists"
01235 
01236       command = datum[0]
01237 
01238       ######################
01239       if command in ("Z", "z"):
01240         x, y, X, Y = None, None, None, None
01241         output.append("Z")
01242 
01243       ######################
01244       elif command in ("H", "h", "V", "v"):
01245         command, num1 = datum
01246 
01247         if command == "H" or (command == "h" and x == None): x = num1
01248         elif command == "h": x += num1
01249         elif command == "V" or (command == "v" and y == None): y = num1
01250         elif command == "v": y += num1
01251 
01252         if trans == None: X, Y = x, y
01253         else: X, Y = trans(x, y)
01254 
01255         output.append("L%g %g" % (X, Y))
01256         
01257       ######################
01258       elif command in ("M", "m", "L", "l", "T", "t"):
01259         command, num1, num2, isglobal12 = datum
01260 
01261         if trans == None or isglobal12:
01262           if command.isupper() or X == None or Y == None:
01263             X, Y = num1, num2
01264           else:
01265             X += num1
01266             Y += num2
01267           x, y = X, Y
01268 
01269         else:
01270           if command.isupper() or x == None or y == None:
01271             x, y = num1, num2
01272           else:
01273             x += num1
01274             y += num2
01275           X, Y = trans(x, y)
01276 
01277         COMMAND = command.capitalize()
01278         output.append("%s%g %g" % (COMMAND, X, Y))
01279 
01280       ######################
01281       elif command in ("S", "s", "Q", "q"):
01282         command, num1, num2, isglobal12, num3, num4, isglobal34 = datum
01283 
01284         if trans == None or isglobal12:
01285           if command.isupper() or X == None or Y == None:
01286             CX, CY = num1, num2
01287           else:
01288             CX = X + num1
01289             CY = Y + num2
01290 
01291         else:
01292           if command.isupper() or x == None or y == None:
01293             cx, cy = num1, num2
01294           else:
01295             cx = x + num1
01296             cy = y + num2
01297           CX, CY = trans(cx, cy)
01298 
01299         if trans == None or isglobal34:
01300           if command.isupper() or X == None or Y == None:
01301             X, Y = num3, num4
01302           else:
01303             X += num3
01304             Y += num4
01305           x, y = X, Y
01306 
01307         else:
01308           if command.isupper() or x == None or y == None:
01309             x, y = num3, num4
01310           else:
01311             x += num3
01312             y += num4
01313           X, Y = trans(x, y)
01314 
01315         COMMAND = command.capitalize()
01316         output.append("%s%g %g %g %g" % (COMMAND, CX, CY, X, Y))
01317 
01318       ######################
01319       elif command in ("C", "c"):
01320         command, num1, num2, isglobal12, num3, num4, isglobal34, num5, num6, isglobal56 = datum
01321 
01322         if trans == None or isglobal12:
01323           if command.isupper() or X == None or Y == None:
01324             C1X, C1Y = num1, num2
01325           else:
01326             C1X = X + num1
01327             C1Y = Y + num2
01328 
01329         else:
01330           if command.isupper() or x == None or y == None:
01331             c1x, c1y = num1, num2
01332           else:
01333             c1x = x + num1
01334             c1y = y + num2
01335           C1X, C1Y = trans(c1x, c1y)
01336 
01337         if trans == None or isglobal34:
01338           if command.isupper() or X == None or Y == None:
01339             C2X, C2Y = num3, num4
01340           else:
01341             C2X = X + num3
01342             C2Y = Y + num4
01343 
01344         else:
01345           if command.isupper() or x == None or y == None:
01346             c2x, c2y = num3, num4
01347           else:
01348             c2x = x + num3
01349             c2y = y + num4
01350           C2X, C2Y = trans(c2x, c2y)
01351 
01352         if trans == None or isglobal56:
01353           if command.isupper() or X == None or Y == None:
01354             X, Y = num5, num6
01355           else:
01356             X += num5
01357             Y += num6
01358           x, y = X, Y
01359 
01360         else:
01361           if command.isupper() or x == None or y == None:
01362             x, y = num5, num6
01363           else:
01364             x += num5
01365             y += num6
01366           X, Y = trans(x, y)
01367 
01368         COMMAND = command.capitalize()
01369         output.append("%s%g %g %g %g %g %g" % (COMMAND, C1X, C1Y, C2X, C2Y, X, Y))
01370 
01371       ######################
01372       elif command in ("A", "a"):
01373         command, num1, num2, isglobal12, angle, large_arc_flag, sweep_flag, num3, num4, isglobal34 = datum
01374 
01375         oldx, oldy = x, y
01376         OLDX, OLDY = X, Y
01377 
01378         if trans == None or isglobal34:
01379           if command.isupper() or X == None or Y == None:
01380             X, Y = num3, num4
01381           else:
01382             X += num3
01383             Y += num4
01384           x, y = X, Y
01385 
01386         else:
01387           if command.isupper() or x == None or y == None:
01388             x, y = num3, num4
01389           else:
01390             x += num3
01391             y += num4
01392           X, Y = trans(x, y)
01393         
01394         if x != None and y != None:
01395           centerx, centery = (x + oldx)/2., (y + oldy)/2.
01396         CENTERX, CENTERY = (X + OLDX)/2., (Y + OLDY)/2.
01397 
01398         if trans == None or isglobal12:
01399           RX = CENTERX + num1
01400           RY = CENTERY + num2
01401 
01402         else:
01403           rx = centerx + num1
01404           ry = centery + num2
01405           RX, RY = trans(rx, ry)
01406 
01407         COMMAND = command.capitalize()
01408         output.append("%s%g %g %g %d %d %g %g" % (COMMAND, RX - CENTERX, RY - CENTERY, angle, large_arc_flag, sweep_flag, X, Y))
01409 
01410       elif command in (",", "."):
01411         command, num1, num2, isglobal12, angle, num3, num4, isglobal34 = datum
01412         if trans == None or isglobal34:
01413           if command == "." or X == None or Y == None:
01414             X, Y = num3, num4
01415           else:
01416             X += num3
01417             Y += num4
01418             x, y = None, None
01419 
01420         else:
01421           if command == "." or x == None or y == None:
01422             x, y = num3, num4
01423           else:
01424             x += num3
01425             y += num4
01426           X, Y = trans(x, y)
01427 
01428         if trans == None or isglobal12:
01429           RX = X + num1
01430           RY = Y + num2
01431 
01432         else:
01433           rx = x + num1
01434           ry = y + num2
01435           RX, RY = trans(rx, ry)
01436 
01437         RX, RY = RX - X, RY - Y
01438 
01439         X1, Y1 = X + RX * math.cos(angle*math.pi/180.), Y + RX * math.sin(angle*math.pi/180.)
01440         X2, Y2 = X + RY * math.sin(angle*math.pi/180.), Y - RY * math.cos(angle*math.pi/180.)
01441         X3, Y3 = X - RX * math.cos(angle*math.pi/180.), Y - RX * math.sin(angle*math.pi/180.)
01442         X4, Y4 = X - RY * math.sin(angle*math.pi/180.), Y + RY * math.cos(angle*math.pi/180.)
01443 
01444         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" \
01445                       % (X1, Y1, RX, RY, angle, X2, Y2, RX, RY, angle, X3, Y3, RX, RY, angle, X4, Y4, RX, RY, angle, X1, Y1))
01446 
01447     return SVG("path", d="".join(output), **self.attr)
01448 
01449 ######################################################################
01450 
01451 def funcRtoC(expr, var="t", globals=None, locals=None):
01452   """Converts a complex "z(t)" string to a function acceptable for Curve.
01453 
01454   expr    required        string in the form "z(t)"
01455   var     default="t"   name of the independent variable
01456   globals default=None    dict of global variables used in the expression;
01457                           you may want to use Python's builtin globals()
01458   locals  default=None    dict of local variables
01459   """
01460   g = cmath.__dict__
01461   if globals != None: g.update(globals)
01462   output = eval("lambda %s: (%s)" % (var, expr), g, locals)
01463   split = lambda z: (z.real, z.imag)
01464   output2 = lambda t: split(output(t))
01465   output2.func_name = "%s -> %s" % (var, expr)
01466   return output2
01467 
01468 def funcRtoR2(expr, var="t", globals=None, locals=None):
01469   """Converts a "f(t), g(t)" string to a function acceptable for Curve.
01470 
01471   expr    required        string in the form "f(t), g(t)"
01472   var     default="t"   name of the independent variable
01473   globals default=None    dict of global variables used in the expression;
01474                           you may want to use Python's builtin globals()
01475   locals  default=None    dict of local variables
01476   """
01477   g = math.__dict__
01478   if globals != None: g.update(globals)
01479   output = eval("lambda %s: (%s)" % (var, expr), g, locals)
01480   output.func_name = "%s -> %s" % (var, expr)
01481   return output
01482 
01483 def funcRtoR(expr, var="x", globals=None, locals=None):
01484   """Converts a "f(x)" string to a function acceptable for Curve.
01485 
01486   expr    required        string in the form "f(x)"
01487   var     default="x"   name of the independent variable
01488   globals default=None    dict of global variables used in the expression;
01489                           you may want to use Python's builtin globals()
01490   locals  default=None    dict of local variables
01491   """
01492   g = math.__dict__
01493   if globals != None: g.update(globals)
01494   output = eval("lambda %s: (%s, %s)" % (var, var, expr), g, locals)
01495   output.func_name = "%s -> %s" % (var, expr)
01496   return output
01497 
01498 class Curve:
01499   """Draws a parametric function as a path.
01500 
01501   Curve(f, low, high, loop, attribute=value)
01502 
01503   f                      required         a Python callable or string in
01504                                           the form "f(t), g(t)"
01505   low, high              required         left and right endpoints
01506   loop                   default=False    if True, connect the endpoints
01507   attribute=value pairs  keyword list     SVG attributes
01508   """
01509   defaults = {}
01510   random_sampling = True
01511   recursion_limit = 15
01512   linearity_limit = 0.05
01513   discontinuity_limit = 5.
01514 
01515   def __repr__(self):
01516     return "<Curve %s [%s, %s] %s>" % (self.f, self.low, self.high, self.attr)
01517 
01518   def __init__(self, f, low, high, loop=False, **attr):
01519     self.f = f
01520     self.low = low
01521     self.high = high
01522     self.loop = loop
01523 
01524     self.attr = dict(self.defaults)
01525     self.attr.update(attr)
01526 
01527   ### nested class Sample
01528   class Sample:
01529     def __repr__(self):
01530       t, x, y, X, Y = self.t, self.x, self.y, self.X, self.Y
01531       if t != None: t = "%g" % t
01532       if x != None: x = "%g" % x
01533       if y != None: y = "%g" % y
01534       if X != None: X = "%g" % X
01535       if Y != None: Y = "%g" % Y
01536       return "<Curve.Sample t=%s x=%s y=%s X=%s Y=%s>" % (t, x, y, X, Y)
01537 
01538     def __init__(self, t): self.t = t
01539 
01540     def link(self, left, right): self.left, self.right = left, right
01541 
01542     def evaluate(self, f, trans):
01543       self.x, self.y = f(self.t)
01544       if trans == None:
01545         self.X, self.Y = self.x, self.y
01546       else:
01547         self.X, self.Y = trans(self.x, self.y)
01548   ### end Sample
01549 
01550   ### nested class Samples
01551   class Samples:
01552     def __repr__(self): return "<Curve.Samples (%d samples)>" % len(self)
01553 
01554     def __init__(self, left, right): self.left, self.right = left, right
01555 
01556     def __len__(self):
01557       count = 0
01558       current = self.left
01559       while current != None:
01560         count += 1
01561         current = current.right
01562       return count
01563 
01564     def __iter__(self):
01565       self.current = self.left
01566       return self
01567 
01568     def next(self):
01569       current = self.current
01570       if current == None: raise StopIteration
01571       self.current = self.current.right
01572       return current
01573   ### end nested class
01574 
01575   def sample(self, trans=None):
01576     """Adaptive-sampling algorithm that chooses the best sample points
01577     for a parametric curve between two endpoints and detects
01578     discontinuities.  Called by SVG()."""
01579     oldrecursionlimit = sys.getrecursionlimit()
01580     sys.setrecursionlimit(self.recursion_limit + 100)
01581     try:
01582       # the best way to keep all the information while sampling is to make a linked list
01583       if not (self.low < self.high): raise ValueError, "low must be less than high"
01584       low, high = self.Sample(float(self.low)), self.Sample(float(self.high))
01585       low.link(None, high)
01586       high.link(low, None)
01587 
01588       low.evaluate(self.f, trans)
01589       high.evaluate(self.f, trans)
01590 
01591       # adaptive sampling between the low and high points
01592       self.subsample(low, high, 0, trans)
01593 
01594       # Prune excess points where the curve is nearly linear
01595       left = low
01596       while left.right != None:
01597         # increment mid and right
01598         mid = left.right
01599         right = mid.right
01600         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:
01601           numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
01602           denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
01603           if denom != 0. and abs(numer/denom) < self.linearity_limit:
01604             # drop mid (the garbage collector will get it)
01605             left.right = right
01606             right.left = left
01607           else:
01608             # increment left
01609             left = left.right
01610         else:
01611           left = left.right
01612 
01613       self.last_samples = self.Samples(low, high)
01614 
01615     finally:
01616       sys.setrecursionlimit(oldrecursionlimit)
01617 
01618   def subsample(self, left, right, depth, trans=None):
01619     """Part of the adaptive-sampling algorithm that chooses the best
01620     sample points.  Called by sample()."""
01621 
01622     if self.random_sampling:
01623       mid = self.Sample(left.t + random.uniform(0.3, 0.7) * (right.t - left.t))
01624     else:
01625       mid = self.Sample(left.t + 0.5 * (right.t - left.t))
01626 
01627     left.right = mid
01628     right.left = mid
01629     mid.link(left, right)
01630     mid.evaluate(self.f, trans)
01631 
01632     # calculate the distance of closest approach of mid to the line between left and right
01633     numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
01634     denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
01635 
01636     # if we haven't sampled enough or left fails to be close enough to right, or mid fails to be linear enough...
01637     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):
01638 
01639       # and we haven't sampled too many points
01640       if depth < self.recursion_limit:
01641         self.subsample(left, mid, depth+1, trans)
01642         self.subsample(mid, right, depth+1, trans)
01643 
01644       else:
01645         # We've sampled many points and yet it's still not a small linear gap.
01646         # Break the line: it's a discontinuity
01647         mid.y = mid.Y = None
01648 
01649   def SVG(self, trans=None):
01650     """Apply the transformation "trans" and return an SVG object."""
01651     return self.Path(trans).SVG()
01652 
01653   def Path(self, trans=None, local=False):
01654     """Apply the transformation "trans" and return a Path object in
01655     global coordinates.  If local=True, return a Path in local coordinates
01656     (which must be transformed again)."""
01657 
01658     if isinstance(trans, basestring): trans = totrans(trans)
01659     if isinstance(self.f, basestring): self.f = funcRtoR2(self.f)
01660 
01661     self.sample(trans)
01662 
01663     output = []
01664     for s in self.last_samples:
01665       if s.X != None and s.Y != None:
01666         if s.left == None or s.left.Y == None:
01667           command = "M"
01668         else:
01669           command = "L"
01670 
01671         if local: output.append((command, s.x, s.y, False))
01672         else: output.append((command, s.X, s.Y, True))
01673 
01674     if self.loop: output.append(("Z",))
01675     return Path(output, **self.attr)
01676 
01677 ######################################################################
01678 
01679 class Poly:
01680   """Draws a curve specified by a sequence of points. The curve may be
01681   piecewise linear, like a polygon, or a Bezier curve.
01682 
01683   Poly(d, mode, loop, attribute=value)
01684 
01685   d                       required        list of tuples representing points
01686                                           and possibly control points
01687   mode                    default="L"   "lines", "bezier", "velocity",
01688                                           "foreback", "smooth", or an abbreviation
01689   loop                    default=False   if True, connect the first and last
01690                                           point, closing the loop
01691   attribute=value pairs   keyword list    SVG attributes
01692 
01693   The format of the tuples in d depends on the mode.
01694 
01695   "lines"/"L"         d=[(x,y), (x,y), ...]
01696                                           piecewise-linear segments joining the (x,y) points
01697   "bezier"/"B"        d=[(x, y, c1x, c1y, c2x, c2y), ...]
01698                                           Bezier curve with two control points (control points
01699                                           preceed (x,y), as in SVG paths). If (c1x,c1y) and
01700                                           (c2x,c2y) both equal (x,y), you get a linear
01701                                           interpolation ("lines")
01702   "velocity"/"V"      d=[(x, y, vx, vy), ...]
01703                                           curve that passes through (x,y) with velocity (vx,vy)
01704                                           (one unit of arclength per unit time); in other words,
01705                                           (vx,vy) is the tangent vector at (x,y). If (vx,vy) is
01706                                           (0,0), you get a linear interpolation ("lines").
01707   "foreback"/"F"      d=[(x, y, bx, by, fx, fy), ...]
01708                                           like "velocity" except that there is a left derivative
01709                                           (bx,by) and a right derivative (fx,fy). If (bx,by)
01710                                           equals (fx,fy) (with no minus sign), you get a
01711                                           "velocity" curve
01712   "smooth"/"S"        d=[(x,y), (x,y), ...]
01713                                           a "velocity" interpolation with (vx,vy)[i] equal to
01714                                           ((x,y)[i+1] - (x,y)[i-1])/2: the minimal derivative
01715   """
01716   defaults = {}
01717 
01718   def __repr__(self):
01719     return "<Poly (%d nodes) mode=%s loop=%s %s>" % (len(self.d), self.mode, repr(self.loop), self.attr)
01720 
01721   def __init__(self, d=[], mode="L", loop=False, **attr):
01722     self.d = list(d)
01723     self.mode = mode
01724     self.loop = loop
01725 
01726     self.attr = dict(self.defaults)
01727     self.attr.update(attr)
01728 
01729   def SVG(self, trans=None):
01730     """Apply the transformation "trans" and return an SVG object."""
01731     return self.Path(trans).SVG()
01732 
01733   def Path(self, trans=None, local=False):
01734     """Apply the transformation "trans" and return a Path object in
01735     global coordinates.  If local=True, return a Path in local coordinates
01736     (which must be transformed again)."""
01737     if isinstance(trans, basestring): trans = totrans(trans)
01738 
01739     if self.mode[0] == "L" or self.mode[0] == "l": mode = "L"
01740     elif self.mode[0] == "B" or self.mode[0] == "b": mode = "B"
01741     elif self.mode[0] == "V" or self.mode[0] == "v": mode = "V"
01742     elif self.mode[0] == "F" or self.mode[0] == "f": mode = "F"
01743     elif self.mode[0] == "S" or self.mode[0] == "s":
01744       mode = "S"
01745 
01746       vx, vy = [0.]*len(self.d), [0.]*len(self.d)
01747       for i in xrange(len(self.d)):
01748         inext = (i+1) % len(self.d)
01749         iprev = (i-1) % len(self.d)
01750 
01751         vx[i] = (self.d[inext][0] - self.d[iprev][0])/2.
01752         vy[i] = (self.d[inext][1] - self.d[iprev][1])/2.
01753         if not self.loop and (i == 0 or i == len(self.d)-1):
01754           vx[i], vy[i] = 0., 0.
01755 
01756     else:
01757       raise ValueError, "mode must be \"lines\", \"bezier\", \"velocity\", \"foreback\", \"smooth\", or an abbreviation"
01758 
01759     d = []
01760     indexes = range(len(self.d))
01761     if self.loop and len(self.d) > 0: indexes.append(0)
01762 
01763     for i in indexes:
01764       inext = (i+1) % len(self.d)
01765       iprev = (i-1) % len(self.d)
01766 
01767       x, y = self.d[i][0], self.d[i][1]
01768 
01769       if trans == None: X, Y = x, y
01770       else: X, Y = trans(x, y)
01771 
01772       if d == []:
01773         if local: d.append(("M", x, y, False))
01774         else: d.append(("M", X, Y, True))
01775 
01776       elif mode == "L":
01777         if local: d.append(("L", x, y, False))
01778         else: d.append(("L", X, Y, True))
01779 
01780       elif mode == "B":
01781         c1x, c1y = self.d[i][2], self.d[i][3]
01782         if trans == None: C1X, C1Y = c1x, c1y
01783         else: C1X, C1Y = trans(c1x, c1y)
01784 
01785         c2x, c2y = self.d[i][4], self.d[i][5]
01786         if trans == None: C2X, C2Y = c2x, c2y
01787         else: C2X, C2Y = trans(c2x, c2y)
01788 
01789         if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
01790         else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
01791 
01792       elif mode == "V":
01793         c1x, c1y = self.d[iprev][2]/3. + self.d[iprev][0], self.d[iprev][3]/3. + self.d[iprev][1]
01794         c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
01795 
01796         if trans == None: C1X, C1Y = c1x, c1y
01797         else: C1X, C1Y = trans(c1x, c1y)
01798         if trans == None: C2X, C2Y = c2x, c2y
01799         else: C2X, C2Y = trans(c2x, c2y)
01800 
01801         if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
01802         else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
01803 
01804       elif mode == "F":
01805         c1x, c1y = self.d[iprev][4]/3. + self.d[iprev][0], self.d[iprev][5]/3. + self.d[iprev][1]
01806         c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
01807 
01808         if trans == None: C1X, C1Y = c1x, c1y
01809         else: C1X, C1Y = trans(c1x, c1y)
01810         if trans == None: C2X, C2Y = c2x, c2y
01811         else: C2X, C2Y = trans(c2x, c2y)
01812 
01813         if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
01814         else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
01815 
01816       elif mode == "S":
01817         c1x, c1y = vx[iprev]/3. + self.d[iprev][0], vy[iprev]/3. + self.d[iprev][1]
01818         c2x, c2y = vx[i]/-3. + x, vy[i]/-3. + y
01819 
01820         if trans == None: C1X, C1Y = c1x, c1y
01821         else: C1X, C1Y = trans(c1x, c1y)
01822         if trans == None: C2X, C2Y = c2x, c2y
01823         else: C2X, C2Y = trans(c2x, c2y)
01824 
01825         if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
01826         else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
01827 
01828     if self.loop and len(self.d) > 0: d.append(("Z",))
01829 
01830     return Path(d, **self.attr)
01831 
01832 ######################################################################
01833 
01834 class Text:
01835   """Draws at text string at a specified point in local coordinates.
01836 
01837   x, y                   required      location of the point in local coordinates
01838   d                      required      text/Unicode string
01839   attribute=value pairs  keyword list  SVG attributes 
01840   """
01841 
01842   defaults = {"stroke":"none", "fill":"black", "font-size":5}
01843 
01844   def __repr__(self):
01845     return "<Text %s at (%g, %g) %s>" % (repr(self.d), self.x, self.y, self.attr)
01846 
01847   def __init__(self, x, y, d, **attr):
01848     self.x = x
01849     self.y = y
01850     self.d = str(d)
01851     self.attr = dict(self.defaults)
01852     self.attr.update(attr)
01853 
01854   def SVG(self, trans=None):
01855     """Apply the transformation "trans" and return an SVG object."""
01856     if isinstance(trans, basestring): trans = totrans(trans)
01857 
01858     X, Y = self.x, self.y
01859     if trans != None: X, Y = trans(X, Y)
01860     return SVG("text", self.d, x=X, y=Y, **self.attr)
01861 
01862 class TextGlobal:
01863   """Draws at text string at a specified point in global coordinates.
01864 
01865   x, y                   required      location of the point in global coordinates
01866   d                      required      text/Unicode string
01867   attribute=value pairs  keyword list  SVG attributes 
01868   """
01869   defaults = {"stroke":"none", "fill":"black", "font-size":5}
01870 
01871   def __repr__(self):
01872     return "<TextGlobal %s at (%s, %s) %s>" % (repr(self.d), str(self.x), str(self.y), self.attr)
01873 
01874   def __init__(self, x, y, d, **attr):
01875     self.x = x
01876     self.y = y
01877     self.d = str(d)
01878     self.attr = dict(self.defaults)
01879     self.attr.update(attr)
01880 
01881   def SVG(self, trans=None):
01882     """Apply the transformation "trans" and return an SVG object."""
01883     return SVG("text", self.d, x=self.x, y=self.y, **self.attr)
01884 
01885 ######################################################################
01886 
01887 _symbol_templates = {"dot": SVG("symbol", SVG("circle", cx=0, cy=0, r=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), \
01888                     "box": SVG("symbol", SVG("rect", x1=-1, y1=-1, x2=1, y2=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), \
01889                     "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"), \
01890                     "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"), \
01891                     }
01892 
01893 def make_symbol(id, shape="dot", **attr):
01894   """Creates a new instance of an SVG symbol to avoid cross-linking objects.
01895 
01896   id                    required         a new identifier (string/Unicode)
01897   shape                 default="dot"  the shape name from _symbol_templates
01898   attribute=value list  keyword list     modify the SVG attributes of the new symbol
01899   """
01900   output = copy.deepcopy(_symbol_templates[shape])
01901   for i in output.sub: i.attr.update(attr_preprocess(attr))
01902   output["id"] = id
01903   return output
01904 
01905 _circular_dot = make_symbol("circular_dot")
01906 
01907 class Dots:
01908   """Dots draws SVG symbols at a set of points.
01909 
01910   d                      required               list of (x,y) points
01911   symbol                 default=None           SVG symbol or a new identifier to
01912                                                 label an auto-generated symbol;
01913                                                 if None, use pre-defined _circular_dot
01914   width, height          default=1, 1           width and height of the symbols
01915                                                 in SVG coordinates
01916   attribute=value pairs  keyword list           SVG attributes
01917   """
01918   defaults = {}
01919 
01920   def __repr__(self):
01921     return "<Dots (%d nodes) %s>" % (len(self.d), self.attr)
01922 
01923   def __init__(self, d=[], symbol=None, width=1., height=1., **attr):
01924     self.d = list(d)
01925     self.width = width
01926     self.height = height
01927 
01928     self.attr = dict(self.defaults)
01929     self.attr.update(attr)
01930 
01931     if symbol == None:
01932       self.symbol = _circular_dot
01933     elif isinstance(symbol, SVG):
01934       self.symbol = symbol
01935     else:
01936       self.symbol = make_symbol(symbol)
01937 
01938   def SVG(self, trans=None):
01939     """Apply the transformation "trans" and return an SVG object."""
01940     if isinstance(trans, basestring): trans = totrans(trans)
01941 
01942     output = SVG("g", SVG("defs", self.symbol))
01943     id = "#%s" % self.symbol["id"]
01944 
01945     for p in self.d:
01946       x, y = p[0], p[1]
01947 
01948       if trans == None: X, Y = x, y
01949       else: X, Y = trans(x, y)
01950 
01951       item = SVG("use", x=X, y=Y, xlink__href=id)
01952       if self.width != None: item["width"] = self.width
01953       if self.height != None: item["height"] = self.height
01954       output.append(item)
01955       
01956     return output
01957 
01958 ######################################################################
01959 
01960 _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"), \
01961                     "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"), \
01962                     }
01963 
01964 def make_marker(id, shape, **attr):
01965   """Creates a new instance of an SVG marker to avoid cross-linking objects.
01966 
01967   id                     required         a new identifier (string/Unicode)
01968   shape                  required         the shape name from _marker_templates
01969   attribute=value list   keyword list     modify the SVG attributes of the new marker
01970   """
01971   output = copy.deepcopy(_marker_templates[shape])
01972   for i in output.sub: i.attr.update(attr_preprocess(attr))
01973   output["id"] = id
01974   return output
01975 
01976 class Line(Curve):
01977   """Draws a line between two points.
01978 
01979   Line(x1, y1, x2, y2, arrow_start, arrow_end, attribute=value)
01980 
01981   x1, y1                  required        the starting point
01982   x2, y2                  required        the ending point
01983   arrow_start             default=None    if an identifier string/Unicode,
01984                                           draw a new arrow object at the
01985                                           beginning of the line; if a marker,
01986                                           draw that marker instead
01987   arrow_end               default=None    same for the end of the line
01988   attribute=value pairs   keyword list    SVG attributes
01989   """
01990   defaults = {}
01991 
01992   def __repr__(self):
01993     return "<Line (%g, %g) to (%g, %g) %s>" % (self.x1, self.y1, self.x2, self.y2, self.attr)
01994 
01995   def __init__(self, x1, y1, x2, y2, arrow_start=None, arrow_end=None, **attr):
01996     self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
01997     self.arrow_start, self.arrow_end = arrow_start, arrow_end       
01998 
01999     self.attr = dict(self.defaults)
02000     self.attr.update(attr)
02001 
02002   def SVG(self, trans=None):
02003     """Apply the transformation "trans" and return an SVG object."""
02004 
02005     line = self.Path(trans).SVG()
02006 
02007     if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
02008       defs = SVG("defs")
02009 
02010       if self.arrow_start != False and self.arrow_start != None:
02011         if isinstance(self.arrow_start, SVG):
02012           defs.append(self.arrow_start)
02013           line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
02014         elif isinstance(self.arrow_start, basestring):
02015           defs.append(make_marker(self.arrow_start, "arrow_start"))
02016           line.attr["marker-start"] = "url(#%s)" % self.arrow_start
02017         else:
02018           raise TypeError, "arrow_start must be False/None or an id string for the new marker"
02019 
02020       if self.arrow_end != False and self.arrow_end != None:
02021         if isinstance(self.arrow_end, SVG):
02022           defs.append(self.arrow_end)
02023           line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
02024         elif isinstance(self.arrow_end, basestring):
02025           defs.append(make_marker(self.arrow_end, "arrow_end"))
02026           line.attr["marker-end"] = "url(#%s)" % self.arrow_end
02027         else:
02028           raise TypeError, "arrow_end must be False/None or an id string for the new marker"
02029 
02030       return SVG("g", defs, line)
02031 
02032     return line
02033 
02034   def Path(self, trans=None, local=False):
02035     """Apply the transformation "trans" and return a Path object in
02036     global coordinates.  If local=True, return a Path in local coordinates
02037     (which must be transformed again)."""
02038     self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1 + t*(self.y2 - self.y1))
02039     self.low = 0.
02040     self.high = 1.
02041     self.loop = False
02042 
02043     if trans == None:
02044       return Path([("M", self.x1, self.y1, not local), ("L", self.x2, self.y2, not local)], **self.attr)
02045     else:
02046       return Curve.Path(self, trans, local)
02047 
02048 class LineGlobal:
02049   """Draws a line between two points, one or both of which is in
02050   global coordinates.
02051 
02052   Line(x1, y1, x2, y2, lcoal1, local2, arrow_start, arrow_end, attribute=value)
02053 
02054   x1, y1                  required        the starting point
02055   x2, y2                  required        the ending point
02056   local1                  default=False   if True, interpret first point as a
02057                                           local coordinate (apply transform)
02058   local2                  default=False   if True, interpret second point as a
02059                                           local coordinate (apply transform)
02060   arrow_start             default=None    if an identifier string/Unicode,
02061                                           draw a new arrow object at the
02062                                           beginning of the line; if a marker,
02063                                           draw that marker instead
02064   arrow_end               default=None    same for the end of the line
02065   attribute=value pairs   keyword list    SVG attributes
02066   """
02067   defaults = {}
02068 
02069   def __repr__(self):
02070     local1, local2 = "", ""
02071     if self.local1: local1 = "L"
02072     if self.local2: local2 = "L"
02073 
02074     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)
02075 
02076   def __init__(self, x1, y1, x2, y2, local1=False, local2=False, arrow_start=None, arrow_end=None, **attr):
02077     self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
02078     self.local1, self.local2 = local1, local2
02079     self.arrow_start, self.arrow_end = arrow_start, arrow_end
02080 
02081     self.attr = dict(self.defaults)
02082     self.attr.update(attr)
02083 
02084   def SVG(self, trans=None):
02085     """Apply the transformation "trans" and return an SVG object."""
02086     if isinstance(trans, basestring): trans = totrans(trans)
02087 
02088     X1, Y1, X2, Y2 = self.x1, self.y1, self.x2, self.y2
02089 
02090     if self.local1: X1, Y1 = trans(X1, Y1)
02091     if self.local2: X2, Y2 = trans(X2, Y2)
02092 
02093     line = SVG("path", d="M%s %s L%s %s" % (X1, Y1, X2, Y2), **self.attr)
02094 
02095     if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
02096       defs = SVG("defs")
02097 
02098       if self.arrow_start != False and self.arrow_start != None:
02099         if isinstance(self.arrow_start, SVG):
02100           defs.append(self.arrow_start)
02101           line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
02102         elif isinstance(self.arrow_start, basestring):
02103           defs.append(make_marker(self.arrow_start, "arrow_start"))
02104           line.attr["marker-start"] = "url(#%s)" % self.arrow_start
02105         else:
02106           raise TypeError, "arrow_start must be False/None or an id string for the new marker"
02107 
02108       if self.arrow_end != False and self.arrow_end != None:
02109         if isinstance(self.arrow_end, SVG):
02110           defs.append(self.arrow_end)
02111           line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
02112         elif isinstance(self.arrow_end, basestring):
02113           defs.append(make_marker(self.arrow_end, "arrow_end"))
02114           line.attr["marker-end"] = "url(#%s)" % self.arrow_end
02115         else:
02116           raise TypeError, "arrow_end must be False/None or an id string for the new marker"
02117 
02118       return SVG("g", defs, line)
02119 
02120     return line
02121 
02122 class VLine(Line):
02123   """Draws a vertical line.
02124 
02125   VLine(y1, y2, x, attribute=value)
02126 
02127   y1, y2                  required        y range
02128   x                       required        x position
02129   attribute=value pairs   keyword list    SVG attributes
02130   """
02131   defaults = {}
02132 
02133   def __repr__(self):
02134     return "<VLine (%g, %g) at x=%s %s>" % (self.y1, self.y2, self.x, self.attr)
02135 
02136   def __init__(self, y1, y2, x, **attr):
02137     self.x = x
02138     self.attr = dict(self.defaults)
02139     self.attr.update(attr)
02140     Line.__init__(self, x, y1, x, y2, **self.attr)
02141 
02142   def Path(self, trans=None, local=False):
02143     """Apply the transformation "trans" and return a Path object in
02144     global coordinates.  If local=True, return a Path in local coordinates
02145     (which must be transformed again)."""
02146     self.x1 = self.x
02147     self.x2 = self.x
02148     return Line.Path(self, trans, local)
02149 
02150 class HLine(Line):
02151   """Draws a horizontal line.
02152 
02153   HLine(x1, x2, y, attribute=value)
02154 
02155   x1, x2                  required        x range
02156   y                       required        y position
02157   attribute=value pairs   keyword list    SVG attributes
02158   """
02159   defaults = {}
02160 
02161   def __repr__(self):
02162     return "<HLine (%g, %g) at y=%s %s>" % (self.x1, self.x2, self.y, self.attr)
02163 
02164   def __init__(self, x1, x2, y, **attr):
02165     self.y = y
02166     self.attr = dict(self.defaults)
02167     self.attr.update(attr)
02168     Line.__init__(self, x1, y, x2, y, **self.attr)
02169 
02170   def Path(self, trans=None, local=False):
02171     """Apply the transformation "trans" and return a Path object in
02172     global coordinates.  If local=True, return a Path in local coordinates
02173     (which must be transformed again)."""
02174     self.y1 = self.y
02175     self.y2 = self.y
02176     return Line.Path(self, trans, local)
02177 
02178 ######################################################################
02179 
02180 class Rect(Curve):
02181   """Draws a rectangle.
02182 
02183   Rect(x1, y1, x2, y2, attribute=value)
02184 
02185   x1, y1                  required        the starting point
02186   x2, y2                  required        the ending point
02187   attribute=value pairs   keyword list    SVG attributes
02188   """
02189   defaults = {}
02190 
02191   def __repr__(self):
02192     return "<Rect (%g, %g), (%g, %g) %s>" % (self.x1, self.y1, self.x2, self.y2, self.attr)
02193 
02194   def __init__(self, x1, y1, x2, y2, **attr):
02195     self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
02196 
02197     self.attr = dict(self.defaults)
02198     self.attr.update(attr)
02199 
02200   def SVG(self, trans=None):
02201     """Apply the transformation "trans" and return an SVG object."""
02202     return self.Path(trans).SVG()
02203 
02204   def Path(self, trans=None, local=False):
02205     """Apply the transformation "trans" and return a Path object in
02206     global coordinates.  If local=True, return a Path in local coordinates
02207     (which must be transformed again)."""
02208     if trans == None:
02209       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)
02210 
02211     else:
02212       self.low = 0.
02213       self.high = 1.
02214       self.loop = False
02215 
02216       self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1)
02217       d1 = Curve.Path(self, trans, local).d
02218 
02219       self.f = lambda t: (self.x2, self.y1 + t*(self.y2 - self.y1))
02220       d2 = Curve.Path(self, trans, local).d
02221       del d2[0]
02222 
02223       self.f = lambda t: (self.x2 + t*(self.x1 - self.x2), self.y2)
02224       d3 = Curve.Path(self, trans, local).d
02225       del d3[0]
02226 
02227       self.f = lambda t: (self.x1, self.y2 + t*(self.y1 - self.y2))
02228       d4 = Curve.Path(self, trans, local).d
02229       del d4[0]
02230 
02231       return Path(d=(d1 + d2 + d3 + d4 + [("Z",)]), **self.attr)
02232 
02233 ######################################################################
02234 
02235 class Ellipse(Curve):
02236   """Draws an ellipse from a semimajor vector (ax,ay) and a semiminor
02237   length (b).
02238 
02239   Ellipse(x, y, ax, ay, b, attribute=value)
02240 
02241   x, y                    required        the center of the ellipse/circle
02242   ax, ay                  required        a vector indicating the length
02243                                           and direction of the semimajor axis
02244   b                       required        the length of the semiminor axis.
02245                                           If equal to sqrt(ax2 + ay2), the
02246                                           ellipse is a circle
02247   attribute=value pairs   keyword list    SVG attributes
02248 
02249   (If sqrt(ax**2 + ay**2) is less than b, then (ax,ay) is actually the
02250   semiminor axis.)
02251   """
02252   defaults = {}
02253 
02254   def __repr__(self):
02255     return "<Ellipse (%g, %g) a=(%g, %g), b=%g %s>" % (self.x, self.y, self.ax, self.ay, self.b, self.attr)
02256 
02257   def __init__(self, x, y, ax, ay, b, **attr):
02258     self.x, self.y, self.ax, self.ay, self.b = x, y, ax, ay, b
02259 
02260     self.attr = dict(self.defaults)
02261     self.attr.update(attr)
02262 
02263   def SVG(self, trans=None):
02264     """Apply the transformation "trans" and return an SVG object."""
02265     return self.Path(trans).SVG()
02266 
02267   def Path(self, trans=None, local=False):
02268     """Apply the transformation "trans" and return a Path object in
02269     global coordinates.  If local=True, return a Path in local coordinates
02270     (which must be transformed again)."""
02271     angle = math.atan2(self.ay, self.ax) + math.pi/2.
02272     bx = self.b * math.cos(angle)
02273     by = self.b * math.sin(angle)
02274 
02275     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))
02276     self.low = -math.pi
02277     self.high = math.pi
02278     self.loop = True
02279     return Curve.Path(self, trans, local)
02280 
02281 ######################################################################
02282 
02283 def unumber(x):
02284   """Converts numbers to a Unicode string, taking advantage of special
02285   Unicode characters to make nice minus signs and scientific notation.
02286   """
02287   output = u"%g" % x
02288 
02289   if output[0] == u"-":
02290     output = u"\u2013" + output[1:]
02291 
02292   index = output.find(u"e")
02293   if index != -1:
02294     uniout = unicode(output[:index]) + u"\u00d710"
02295     saw_nonzero = False
02296     for n in output[index+1:]:
02297       if n == u"+": pass # uniout += u"\u207a"
02298       elif n == u"-": uniout += u"\u207b"
02299       elif n == u"0":
02300         if saw_nonzero: uniout += u"\u2070"
02301       elif n == u"1":
02302         saw_nonzero = True
02303         uniout += u"\u00b9"
02304       elif n == u"2":
02305         saw_nonzero = True
02306         uniout += u"\u00b2"
02307       elif n == u"3":
02308         saw_nonzero = True
02309         uniout += u"\u00b3"
02310       elif u"4" <= n <= u"9":
02311         saw_nonzero = True
02312         if saw_nonzero: uniout += eval("u\"\\u%x\"" % (0x2070 + ord(n) - ord(u"0")))
02313       else: uniout += n
02314 
02315     if uniout[:2] == u"1\u00d7": uniout = uniout[2:]
02316     return uniout
02317 
02318   return output
02319 
02320 class Ticks:
02321   """Superclass for all graphics primatives that draw ticks,
02322   miniticks, and tick labels.  This class only draws the ticks.
02323 
02324   Ticks(f, low, high, ticks, miniticks, labels, logbase, arrow_start,
02325         arrow_end, text_attr, attribute=value)
02326 
02327   f                       required        parametric function along which ticks
02328                                           will be drawn; has the same format as
02329                                           the function used in Curve
02330   low, high               required        range of the independent variable
02331   ticks                   default=-10     request ticks according to the standard
02332                                           tick specification (see below)
02333   miniticks               default=True    request miniticks according to the
02334                                           standard minitick specification (below)
02335   labels                  True            request tick labels according to the
02336                                           standard tick label specification (below)
02337   logbase                 default=None    if a number, the axis is logarithmic with
02338                                           ticks at the given base (usually 10)
02339   arrow_start             default=None    if a new string identifier, draw an arrow
02340                                           at the low-end of the axis, referenced by
02341                                           that identifier; if an SVG marker object,
02342                                           use that marker
02343   arrow_end               default=None    if a new string identifier, draw an arrow
02344                                           at the high-end of the axis, referenced by
02345                                           that identifier; if an SVG marker object,
02346                                           use that marker
02347   text_attr               default={}      SVG attributes for the text labels
02348   attribute=value pairs   keyword list    SVG attributes for the tick marks 
02349 
02350   Standard tick specification:
02351 
02352       * True: same as -10 (below).
02353       * Positive number N: draw exactly N ticks, including the endpoints. To
02354         subdivide an axis into 10 equal-sized segments, ask for 11 ticks.
02355       * Negative number -N: draw at least N ticks. Ticks will be chosen with
02356         "natural" values, multiples of 2 or 5.
02357       * List of values: draw a tick mark at each value.
02358       * Dict of value, label pairs: draw a tick mark at each value, labeling
02359         it with the given string. This lets you say things like {3.14159: "pi"}.
02360       * False or None: no ticks.
02361 
02362   Standard minitick specification:
02363 
02364       * True: draw miniticks with "natural" values, more closely spaced than
02365         the ticks.
02366       * Positive number N: draw exactly N miniticks, including the endpoints.
02367         To subdivide an axis into 100 equal-sized segments, ask for 101 miniticks.
02368       * Negative number -N: draw at least N miniticks.
02369       * List of values: draw a minitick mark at each value.
02370       * False or None: no miniticks. 
02371 
02372   Standard tick label specification:
02373 
02374       * True: use the unumber function (described below)
02375       * Format string: standard format strings, e.g. "%5.2f" for 12.34
02376       * Python callable: function that converts numbers to strings
02377       * False or None: no labels 
02378   """
02379   defaults = {"stroke-width":"0.25pt"}
02380   text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
02381   tick_start = -1.5
02382   tick_end = 1.5
02383   minitick_start = -0.75
02384   minitick_end = 0.75
02385   text_start = 2.5
02386   text_angle = 0.
02387 
02388   def __repr__(self):
02389     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)
02390 
02391   def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, text_attr={}, **attr):
02392     self.f = f
02393     self.low = low
02394     self.high = high
02395     self.ticks = ticks
02396     self.miniticks = miniticks
02397     self.labels = labels
02398     self.logbase = logbase
02399     self.arrow_start = arrow_start
02400     self.arrow_end = arrow_end
02401 
02402     self.attr = dict(self.defaults)
02403     self.attr.update(attr)
02404 
02405     self.text_attr = dict(self.text_defaults)
02406     self.text_attr.update(text_attr)
02407 
02408   def orient_tickmark(self, t, trans=None):
02409     """Return the position, normalized local x vector, normalized
02410     local y vector, and angle of a tick at position t.
02411 
02412     Normally only used internally.
02413     """
02414     if isinstance(trans, basestring): trans = totrans(trans)
02415     if trans == None:
02416       f = self.f
02417     else:
02418       f = lambda t: trans(*self.f(t))
02419 
02420     eps = _epsilon * abs(self.high - self.low)
02421 
02422     X, Y = f(t)
02423     Xprime, Yprime = f(t + eps)
02424     xhatx, xhaty = (Xprime - X)/eps, (Yprime - Y)/eps
02425 
02426     norm = math.sqrt(xhatx**2 + xhaty**2)
02427     if norm != 0: xhatx, xhaty = xhatx/norm, xhaty/norm
02428     else: xhatx, xhaty = 1., 0.
02429 
02430     angle = math.atan2(xhaty, xhatx) + math.pi/2.
02431     yhatx, yhaty = math.cos(angle), math.sin(angle)
02432 
02433     return (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle
02434 
02435   def SVG(self, trans=None):
02436     """Apply the transformation "trans" and return an SVG object."""
02437     if isinstance(trans, basestring): trans = totrans(trans)
02438 
02439     self.last_ticks, self.last_miniticks = self.interpret()
02440     tickmarks = Path([], **self.attr)
02441     minitickmarks = Path([], **self.attr)
02442     output = SVG("g")
02443 
02444     if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
02445       defs = SVG("defs")
02446 
02447       if self.arrow_start != False and self.arrow_start != None:
02448         if isinstance(self.arrow_start, SVG):
02449           defs.append(self.arrow_start)
02450         elif isinstance(self.arrow_start, basestring):
02451           defs.append(make_marker(self.arrow_start, "arrow_start"))
02452         else:
02453           raise TypeError, "arrow_start must be False/None or an id string for the new marker"
02454 
02455       if self.arrow_end != False and self.arrow_end != None:
02456         if isinstance(self.arrow_end, SVG):
02457           defs.append(self.arrow_end)
02458         elif isinstance(self.arrow_end, basestring):
02459           defs.append(make_marker(self.arrow_end, "arrow_end"))
02460         else:
02461           raise TypeError, "arrow_end must be False/None or an id string for the new marker"
02462 
02463       output.append(defs)
02464 
02465     eps = _epsilon * (self.high - self.low)
02466 
02467     for t, label in self.last_ticks.items():
02468       (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
02469       
02470       if (not self.arrow_start or abs(t - self.low) > eps) and (not self.arrow_end or abs(t - self.high) > eps):
02471         tickmarks.d.append(("M", X - yhatx*self.tick_start, Y - yhaty*self.tick_start, True))
02472         tickmarks.d.append(("L", X - yhatx*self.tick_end, Y - yhaty*self.tick_end, True))
02473 
02474       angle = (angle - math.pi/2.)*180./math.pi + self.text_angle
02475 
02476       ########### a HACK! ############ (to be removed when Inkscape handles baselines)
02477       if _hacks["inkscape-text-vertical-shift"]:
02478         if self.text_start > 0:
02479           X += math.cos(angle*math.pi/180. + math.pi/2.) * 2.
02480           Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2.
02481         else:
02482           X += math.cos(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
02483           Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
02484       ########### end hack ###########
02485 
02486       if label != "":
02487         output.append(SVG("text", label, transform="translate(%g, %g) rotate(%g)" % \
02488                           (X - yhatx*self.text_start, Y - yhaty*self.text_start, angle), **self.text_attr))
02489 
02490     for t in self.last_miniticks:
02491       skip = False
02492       for tt in self.last_ticks.keys():
02493         if abs(t - tt) < eps:
02494           skip = True
02495           break
02496       if not skip:
02497         (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
02498 
02499       if (not self.arrow_start or abs(t - self.low) > eps) and (not self.arrow_end or abs(t - self.high) > eps):
02500         minitickmarks.d.append(("M", X - yhatx*self.minitick_start, Y - yhaty*self.minitick_start, True))
02501         minitickmarks.d.append(("L", X - yhatx*self.minitick_end, Y - yhaty*self.minitick_end, True))
02502 
02503     output.prepend(tickmarks.SVG(trans))
02504     output.prepend(minitickmarks.SVG(trans))
02505     return output
02506 
02507   def interpret(self):
02508     """Evaluate and return optimal ticks and miniticks according to
02509     the standard minitick specification.
02510 
02511     Normally only used internally.
02512     """
02513 
02514     if self.labels == None or self.labels == False:
02515       format = lambda x: ""
02516 
02517     elif self.labels == True:
02518       format = unumber
02519 
02520     elif isinstance(self.labels, basestring):
02521       format = lambda x: (self.labels % x)
02522 
02523     elif callable(self.labels):
02524       format = self.labels
02525 
02526     else: raise TypeError, "labels must be None/False, True, a format string, or a number->string function"
02527 
02528     # Now for the ticks
02529     ticks = self.ticks
02530 
02531     # Case 1: ticks is None/False
02532     if ticks == None or ticks == False: return {}, []
02533 
02534     # Case 2: ticks is the number of desired ticks
02535     elif isinstance(ticks, (int, long)):
02536       if ticks == True: ticks = -10
02537 
02538       if self.logbase == None:
02539         ticks = self.compute_ticks(ticks, format)
02540       else:
02541         ticks = self.compute_logticks(self.logbase, ticks, format)
02542 
02543       # Now for the miniticks
02544       if self.miniticks == True:
02545         if self.logbase == None:
02546           return ticks, self.compute_miniticks(ticks)
02547         else:
02548           return ticks, self.compute_logminiticks(self.logbase)
02549 
02550       elif isinstance(self.miniticks, (int, long)):
02551         return ticks, self.regular_miniticks(self.miniticks)
02552 
02553       elif getattr(self.miniticks, "__iter__", False):
02554         return ticks, self.miniticks
02555 
02556       elif self.miniticks == False or self.miniticks == None:
02557         return ticks, []
02558 
02559       else:
02560         raise TypeError, "miniticks must be None/False, True, a number of desired miniticks, or a list of numbers"
02561         
02562     # Cases 3 & 4: ticks is iterable
02563     elif getattr(ticks, "__iter__", False):
02564 
02565       # Case 3: ticks is some kind of list
02566       if not isinstance(ticks, dict):
02567         output = {}
02568         eps = _epsilon * (self.high - self.low)
02569         for x in ticks:
02570           if format == unumber and abs(x) < eps:
02571             output[x] = u"0"
02572           else:
02573             output[x] = format(x)
02574         ticks = output
02575 
02576       # Case 4: ticks is a dict
02577       else: pass
02578 
02579       # Now for the miniticks
02580       if self.miniticks == True:
02581         if self.logbase == None:
02582           return ticks, self.compute_miniticks(ticks)
02583         else:
02584           return ticks, self.compute_logminiticks(self.logbase)
02585 
02586       elif isinstance(self.miniticks, (int, long)):
02587         return ticks, self.regular_miniticks(self.miniticks)
02588 
02589       elif getattr(self.miniticks, "__iter__", False):
02590         return ticks, self.miniticks
02591 
02592       elif self.miniticks == False or self.miniticks == None:
02593         return ticks, []
02594 
02595       else:
02596         raise TypeError, "miniticks must be None/False, True, a number of desired miniticks, or a list of numbers"
02597         
02598     else:
02599       raise TypeError, "ticks must be None/False, a number of desired ticks, a list of numbers, or a dictionary of explicit markers"
02600 
02601   def compute_ticks(self, N, format):
02602     """Return less than -N or exactly N optimal linear ticks.
02603 
02604     Normally only used internally.
02605     """
02606     if self.low >= self.high: raise ValueError, "low must be less than high"
02607     if N == 1: raise ValueError, "N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum"
02608 
02609     eps = _epsilon * (self.high - self.low)
02610 
02611     if N >= 0:
02612       output = {}
02613       x = self.low
02614       for i in xrange(N):
02615         if format == unumber and abs(x) < eps: label = u"0"
02616         else: label = format(x)
02617         output[x] = label
02618         x += (self.high - self.low)/(N-1.)
02619       return output
02620 
02621     N = -N
02622 
02623     counter = 0
02624     granularity = 10**math.ceil(math.log10(max(abs(self.low), abs(self.high))))
02625     lowN = math.ceil(1.*self.low / granularity)
02626     highN = math.floor(1.*self.high / granularity)
02627 
02628     while (lowN > highN):
02629       countermod3 = counter % 3
02630       if countermod3 == 0: granularity *= 0.5
02631       elif countermod3 == 1: granularity *= 0.4
02632       else: granularity *= 0.5
02633       counter += 1
02634       lowN = math.ceil(1.*self.low / granularity)
02635       highN = math.floor(1.*self.high / granularity)
02636 
02637     last_granularity = granularity
02638     last_trial = None
02639 
02640     while True:
02641       trial = {}
02642       for n in range(int(lowN), int(highN)+1):
02643         x = n * granularity
02644         if format == unumber and abs(x) < eps: label = u"0"
02645         else: label = format(x)
02646         trial[x] = label
02647 
02648       if int(highN)+1 - int(lowN) >= N:
02649         if last_trial == None:
02650           v1, v2 = self.low, self.high
02651           return {v1: format(v1), v2: format(v2)}
02652         else:
02653           low_in_ticks, high_in_ticks = False, False
02654           for t in last_trial.keys():
02655             if 1.*abs(t - self.low)/last_granularity < _epsilon: low_in_ticks = True
02656             if 1.*abs(t - self.high)/last_granularity < _epsilon: high_in_ticks = True
02657 
02658           lowN = 1.*self.low / last_granularity
02659           highN = 1.*self.high / last_granularity
02660           if abs(lowN - round(lowN)) < _epsilon and not low_in_ticks:
02661             last_trial[self.low] = format(self.low)
02662           if abs(highN - round(highN)) < _epsilon and not high_in_ticks:
02663             last_trial[self.high] = format(self.high)
02664           return last_trial
02665 
02666       last_granularity = granularity
02667       last_trial = trial
02668 
02669       countermod3 = counter % 3
02670       if countermod3 == 0: granularity *= 0.5
02671       elif countermod3 == 1: granularity *= 0.4
02672       else: granularity *= 0.5
02673       counter += 1
02674       lowN = math.ceil(1.*self.low / granularity)
02675       highN = math.floor(1.*self.high / granularity)
02676 
02677   def regular_miniticks(self, N):
02678     """Return exactly N linear ticks.
02679 
02680     Normally only used internally.
02681     """
02682     output = []
02683     x = self.low
02684     for i in xrange(N):
02685       output.append(x)
02686       x += (self.high - self.low)/(N-1.)
02687     return output
02688 
02689   def compute_miniticks(self, original_ticks):
02690     """Return optimal linear miniticks, given a set of ticks.
02691 
02692     Normally only used internally.
02693     """
02694     if len(original_ticks) < 2: original_ticks = ticks(self.low, self.high)
02695     original_ticks = original_ticks.keys()
02696     original_ticks.sort()
02697 
02698     if self.low > original_ticks[0] + _epsilon or self.high < original_ticks[-1] - _epsilon:
02699       raise ValueError, "original_ticks {%g...%g} extend beyond [%g, %g]" % (original_ticks[0], original_ticks[-1], self.low, self.high)
02700 
02701     granularities = []
02702     for i in range(len(original_ticks)-1):
02703       granularities.append(original_ticks[i+1] - original_ticks[i])
02704     spacing = 10**(math.ceil(math.log10(min(granularities)) - 1))
02705 
02706     output = []
02707     x = original_ticks[0] - math.ceil(1.*(original_ticks[0] - self.low) / spacing) * spacing
02708 
02709     while x <= self.high:
02710       if x >= self.low:
02711         already_in_ticks = False
02712         for t in original_ticks:
02713           if abs(x-t) < _epsilon * (self.high - self.low): already_in_ticks = True
02714         if not already_in_ticks: output.append(x)
02715       x += spacing
02716     return output
02717 
02718   def compute_logticks(self, base, N, format):
02719     """Return less than -N or exactly N optimal logarithmic ticks.
02720 
02721     Normally only used internally.
02722     """
02723     if self.low >= self.high: raise ValueError, "low must be less than high"
02724     if N == 1: raise ValueError, "N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum"
02725 
02726     eps = _epsilon * (self.high - self.low)
02727 
02728     if N >= 0:
02729       output = {}
02730       x = self.low
02731       for i in xrange(N):
02732         if format == unumber and abs(x) < eps: label = u"0"
02733         else: label = format(x)
02734         output[x] = label
02735         x += (self.high - self.low)/(N-1.)
02736       return output
02737 
02738     N = -N
02739 
02740     lowN = math.floor(math.log(self.low, base))
02741     highN = math.ceil(math.log(self.high, base))
02742     output = {}
02743     for n in range(int(lowN), int(highN)+1):
02744       x = base**n
02745       label = format(x)
02746       if self.low <= x <= self.high: output[x] = label
02747 
02748     for i in range(1, len(output)):
02749       keys = output.keys()
02750       keys.sort()
02751       keys = keys[::i]
02752       values = map(lambda k: output[k], keys)
02753       if len(values) <= N:
02754         for k in output.keys():
02755           if k not in keys:
02756             output[k] = ""
02757         break
02758 
02759     if len(output) <= 2:
02760       output2 = self.compute_ticks(N=-int(math.ceil(N/2.)), format=format)
02761       lowest = min(output2)
02762 
02763       for k in output:
02764         if k < lowest: output2[k] = output[k]
02765       output = output2
02766 
02767     return output
02768 
02769   def compute_logminiticks(self, base):
02770     """Return optimal logarithmic miniticks, given a set of ticks.
02771 
02772     Normally only used internally.
02773     """
02774     if self.low >= self.high: raise ValueError, "low must be less than high"
02775 
02776     lowN = math.floor(math.log(self.low, base))
02777     highN = math.ceil(math.log(self.high, base))
02778     output = []
02779     num_ticks = 0
02780     for n in range(int(lowN), int(highN)+1):
02781       x = base**n
02782       if self.low <= x <= self.high: num_ticks += 1
02783       for m in range(2, int(math.ceil(base))):
02784         minix = m * x
02785         if self.low <= minix <= self.high: output.append(minix)
02786 
02787     if num_ticks <= 2: return []
02788     else: return output
02789 
02790 ######################################################################
02791 
02792 class CurveAxis(Curve, Ticks):
02793   """Draw an axis with tick marks along a parametric curve.
02794 
02795   CurveAxis(f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
02796   text_attr, attribute=value)
02797 
02798   f                      required         a Python callable or string in
02799                                           the form "f(t), g(t)", just like Curve
02800   low, high              required         left and right endpoints
02801   ticks                  default=-10      request ticks according to the standard
02802                                           tick specification (see help(Ticks))
02803   miniticks              default=True     request miniticks according to the
02804                                           standard minitick specification
02805   labels                 True             request tick labels according to the
02806                                           standard tick label specification
02807   logbase                default=None     if a number, the x axis is logarithmic
02808                                           with ticks at the given base (10 being
02809                                           the most common)
02810   arrow_start            default=None     if a new string identifier, draw an
02811                                           arrow at the low-end of the axis,
02812                                           referenced by that identifier; if an
02813                                           SVG marker object, use that marker
02814   arrow_end              default=None     if a new string identifier, draw an
02815                                           arrow at the high-end of the axis,
02816                                           referenced by that identifier; if an
02817                                           SVG marker object, use that marker
02818   text_attr              default={}       SVG attributes for the text labels
02819   attribute=value pairs  keyword list     SVG attributes
02820   """
02821   defaults = {"stroke-width":"0.25pt"}
02822   text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
02823 
02824   def __repr__(self):
02825     return "<CurveAxis %s [%s, %s] ticks=%s labels=%s %s>" % (self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr)
02826 
02827   def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, text_attr={}, **attr):
02828     tattr = dict(self.text_defaults)
02829     tattr.update(text_attr)
02830     Curve.__init__(self, f, low, high)
02831     Ticks.__init__(self, f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
02832 
02833   def SVG(self, trans=None):
02834     """Apply the transformation "trans" and return an SVG object."""
02835     func = Curve.SVG(self, trans)
02836     ticks = Ticks.SVG(self, trans) # returns a <g />
02837 
02838     if self.arrow_start != False and self.arrow_start != None:
02839       if isinstance(self.arrow_start, basestring):
02840         func.attr["marker-start"] = "url(#%s)" % self.arrow_start
02841       else:
02842         func.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
02843 
02844     if self.arrow_end != False and self.arrow_end != None:
02845       if isinstance(self.arrow_end, basestring):
02846         func.attr["marker-end"] = "url(#%s)" % self.arrow_end
02847       else:
02848         func.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
02849 
02850     ticks.append(func)
02851     return ticks
02852 
02853 class LineAxis(Line, Ticks):
02854   """Draws an axis with tick marks along a line.
02855 
02856   LineAxis(x1, y1, x2, y2, start, end, ticks, miniticks, labels, logbase,
02857   arrow_start, arrow_end, text_attr, attribute=value)
02858 
02859   x1, y1                  required        starting point
02860   x2, y2                  required        ending point
02861   start, end              default=0, 1    values to start and end labeling
02862   ticks                   default=-10     request ticks according to the standard
02863                                           tick specification (see help(Ticks))
02864   miniticks               default=True    request miniticks according to the
02865                                           standard minitick specification
02866   labels                  True            request tick labels according to the
02867                                           standard tick label specification
02868   logbase                 default=None    if a number, the x axis is logarithmic
02869                                           with ticks at the given base (usually 10)
02870   arrow_start             default=None    if a new string identifier, draw an arrow
02871                                           at the low-end of the axis, referenced by
02872                                           that identifier; if an SVG marker object,
02873                                           use that marker
02874   arrow_end               default=None    if a new string identifier, draw an arrow
02875                                           at the high-end of the axis, referenced by
02876                                           that identifier; if an SVG marker object,
02877                                           use that marker
02878   text_attr               default={}      SVG attributes for the text labels
02879   attribute=value pairs   keyword list    SVG attributes
02880   """
02881   defaults = {"stroke-width":"0.25pt"}
02882   text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
02883 
02884   def __repr__(self):
02885     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)
02886 
02887   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):
02888     self.start = start
02889     self.end = end
02890     self.exclude = exclude
02891     tattr = dict(self.text_defaults)
02892     tattr.update(text_attr)
02893     Line.__init__(self, x1, y1, x2, y2, **attr)
02894     Ticks.__init__(self, None, None, None, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
02895 
02896   def interpret(self):
02897     if self.exclude != None and not (isinstance(self.exclude, (tuple, list)) and len(self.exclude) == 2 and \
02898                                      isinstance(self.exclude[0], (int, long, float)) and isinstance(self.exclude[1], (int, long, float))):
02899       raise TypeError, "exclude must either be None or (low, high)"
02900 
02901     ticks, miniticks = Ticks.interpret(self)
02902     if self.exclude == None: return ticks, miniticks
02903 
02904     ticks2 = {}
02905     for loc, label in ticks.items():
02906       if self.exclude[0] <= loc <= self.exclude[1]:
02907         ticks2[loc] = ""
02908       else:
02909         ticks2[loc] = label
02910 
02911     return ticks2, miniticks
02912 
02913   def SVG(self, trans=None):
02914     """Apply the transformation "trans" and return an SVG object."""
02915     line = Line.SVG(self, trans) # must be evaluated first, to set self.f, self.low, self.high
02916 
02917     f01 = self.f
02918     self.f = lambda t: f01(1. * (t - self.start) / (self.end - self.start))
02919     self.low = self.start
02920     self.high = self.end
02921 
02922     if self.arrow_start != False and self.arrow_start != None:
02923       if isinstance(self.arrow_start, basestring):
02924         line.attr["marker-start"] = "url(#%s)" % self.arrow_start
02925       else:
02926         line.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
02927 
02928     if self.arrow_end != False and self.arrow_end != None:
02929       if isinstance(self.arrow_end, basestring):
02930         line.attr["marker-end"] = "url(#%s)" % self.arrow_end
02931       else:
02932         line.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
02933 
02934     ticks = Ticks.SVG(self, trans) # returns a <g />
02935     ticks.append(line)
02936     return ticks
02937   
02938 class XAxis(LineAxis):
02939   """Draws an x axis with tick marks.
02940 
02941   XAxis(xmin, xmax, aty, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
02942   exclude, text_attr, attribute=value)
02943 
02944   xmin, xmax              required        the x range
02945   aty                     default=0       y position to draw the axis
02946   ticks                   default=-10     request ticks according to the standard
02947                                           tick specification (see help(Ticks))
02948   miniticks               default=True    request miniticks according to the
02949                                           standard minitick specification
02950   labels                  True            request tick labels according to the
02951                                           standard tick label specification
02952   logbase                 default=None    if a number, the x axis is logarithmic
02953                                           with ticks at the given base (usually 10)
02954   arrow_start             default=None    if a new string identifier, draw an arrow
02955                                           at the low-end of the axis, referenced by
02956                                           that identifier; if an SVG marker object,
02957                                           use that marker
02958   arrow_end               default=None    if a new string identifier, draw an arrow
02959                                           at the high-end of the axis, referenced by
02960                                           that identifier; if an SVG marker object,
02961                                           use that marker
02962   exclude                 default=None    if a (low, high) pair, don't draw text
02963                                           labels within this range
02964   text_attr               default={}      SVG attributes for the text labels
02965   attribute=value pairs   keyword list    SVG attributes for all lines
02966 
02967   The exclude option is provided for Axes to keep text from overlapping
02968   where the axes cross. Normal users are not likely to need it.
02969   """
02970   defaults = {"stroke-width":"0.25pt"}
02971   text_defaults = {"stroke":"none", "fill":"black", "font-size":5, "dominant-baseline":"text-before-edge"}
02972   text_start = -1.
02973   text_angle = 0.
02974 
02975   def __repr__(self):
02976     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)
02977 
02978   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):
02979     self.aty = aty
02980     tattr = dict(self.text_defaults)
02981     tattr.update(text_attr)
02982     LineAxis.__init__(self, xmin, aty, xmax, aty, xmin, xmax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
02983 
02984   def SVG(self, trans=None):
02985     """Apply the transformation "trans" and return an SVG object."""
02986     self.y1 = self.aty
02987     self.y2 = self.aty
02988     return LineAxis.SVG(self, trans)
02989 
02990 class YAxis(LineAxis):
02991   """Draws a y axis with tick marks.
02992 
02993   YAxis(ymin, ymax, atx, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
02994   exclude, text_attr, attribute=value)
02995 
02996   ymin, ymax              required        the y range
02997   atx                     default=0       x position to draw the axis
02998   ticks                   default=-10     request ticks according to the standard
02999                                           tick specification (see help(Ticks))
03000   miniticks               default=True    request miniticks according to the
03001                                           standard minitick specification
03002   labels                  True            request tick labels according to the
03003                                           standard tick label specification
03004   logbase                 default=None    if a number, the y axis is logarithmic
03005                                           with ticks at the given base (usually 10)
03006   arrow_start             default=None    if a new string identifier, draw an arrow
03007                                           at the low-end of the axis, referenced by
03008                                           that identifier; if an SVG marker object,
03009                                           use that marker
03010   arrow_end               default=None    if a new string identifier, draw an arrow
03011                                           at the high-end of the axis, referenced by
03012                                           that identifier; if an SVG marker object,
03013                                           use that marker
03014   exclude                 default=None    if a (low, high) pair, don't draw text
03015                                           labels within this range
03016   text_attr               default={}      SVG attributes for the text labels
03017   attribute=value pairs   keyword list    SVG attributes for all lines
03018 
03019   The exclude option is provided for Axes to keep text from overlapping
03020   where the axes cross. Normal users are not likely to need it.
03021   """
03022   defaults = {"stroke-width":"0.25pt"}
03023   text_defaults = {"stroke":"none", "fill":"black", "font-size":5, "text-anchor":"end", "dominant-baseline":"middle"}
03024   text_start = 2.5
03025   text_angle = 90.
03026 
03027   def __repr__(self):
03028     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)
03029 
03030   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):
03031     self.atx = atx
03032     tattr = dict(self.text_defaults)
03033     tattr.update(text_attr)
03034     LineAxis.__init__(self, atx, ymin, atx, ymax, ymin, ymax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
03035 
03036   def SVG(self, trans=None):
03037     """Apply the transformation "trans" and return an SVG object."""
03038     self.x1 = self.atx
03039     self.x2 = self.atx
03040     return LineAxis.SVG(self, trans)
03041 
03042 class Axes:
03043   """Draw a pair of intersecting x-y axes.
03044 
03045   Axes(xmin, xmax, ymin, ymax, atx, aty, xticks, xminiticks, xlabels, xlogbase,
03046   yticks, yminiticks, ylabels, ylogbase, arrows, text_attr, attribute=value)
03047 
03048   xmin, xmax               required       the x range
03049   ymin, ymax               required       the y range
03050   atx, aty                 default=0, 0   point where the axes try to cross;
03051                                           if outside the range, the axes will
03052                                           cross at the closest corner
03053   xticks                   default=-10    request ticks according to the standard
03054                                           tick specification (see help(Ticks))
03055   xminiticks               default=True   request miniticks according to the
03056                                           standard minitick specification
03057   xlabels                  True           request tick labels according to the
03058                                           standard tick label specification
03059   xlogbase                 default=None   if a number, the x axis is logarithmic
03060                                           with ticks at the given base (usually 10)
03061   yticks                   default=-10    request ticks according to the standard
03062                                           tick specification
03063   yminiticks               default=True   request miniticks according to the
03064                                           standard minitick specification
03065   ylabels                  True           request tick labels according to the
03066                                           standard tick label specification
03067   ylogbase                 default=None   if a number, the y axis is logarithmic
03068                                           with ticks at the given base (usually 10)
03069   arrows                   default=None   if a new string identifier, draw arrows
03070                                           referenced by that identifier
03071   text_attr                default={}     SVG attributes for the text labels
03072   attribute=value pairs    keyword list   SVG attributes for all lines
03073   """
03074   defaults = {"stroke-width":"0.25pt"}
03075   text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
03076 
03077   def __repr__(self):
03078     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)
03079 
03080   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):
03081     self.xmin, self.xmax = xmin, xmax
03082     self.ymin, self.ymax = ymin, ymax
03083     self.atx, self.aty = atx, aty
03084     self.xticks, self.xminiticks, self.xlabels, self.xlogbase = xticks, xminiticks, xlabels, xlogbase
03085     self.yticks, self.yminiticks, self.ylabels, self.ylogbase = yticks, yminiticks, ylabels, ylogbase
03086     self.arrows = arrows
03087 
03088     self.text_attr = dict(self.text_defaults)
03089     self.text_attr.update(text_attr)
03090 
03091     self.attr = dict(self.defaults)
03092     self.attr.update(attr)
03093 
03094   def SVG(self, trans=None):
03095     """Apply the transformation "trans" and return an SVG object."""
03096     atx, aty = self.atx, self.aty
03097     if atx < self.xmin: atx = self.xmin
03098     if atx > self.xmax: atx = self.xmax
03099     if aty < self.ymin: aty = self.ymin
03100     if aty > self.ymax: aty = self.ymax
03101 
03102     xmargin = 0.1 * abs(self.ymin - self.ymax)
03103     xexclude = atx - xmargin, atx + xmargin
03104     
03105     ymargin = 0.1 * abs(self.xmin - self.xmax)
03106     yexclude = aty - ymargin, aty + ymargin
03107 
03108     if self.arrows != None and self.arrows != False:
03109       xarrow_start = self.arrows + ".xstart"
03110       xarrow_end = self.arrows + ".xend"
03111       yarrow_start = self.arrows + ".ystart"
03112       yarrow_end = self.arrows + ".yend"
03113     else:
03114       xarrow_start = xarrow_end = yarrow_start = yarrow_end = None
03115 
03116     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)
03117     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)
03118     return SVG("g", *(xaxis.sub + yaxis.sub))
03119 
03120 ######################################################################
03121 
03122 class HGrid(Ticks):
03123   """Draws the horizontal lines of a grid over a specified region
03124   using the standard tick specification (see help(Ticks)) to place the
03125   grid lines.
03126 
03127   HGrid(xmin, xmax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
03128 
03129   xmin, xmax              required        the x range
03130   low, high               required        the y range
03131   ticks                   default=-10     request ticks according to the standard
03132                                           tick specification (see help(Ticks))
03133   miniticks               default=False   request miniticks according to the
03134                                           standard minitick specification
03135   logbase                 default=None    if a number, the axis is logarithmic
03136                                           with ticks at the given base (usually 10)
03137   mini_attr               default={}      SVG attributes for the minitick-lines
03138                                           (if miniticks != False)
03139   attribute=value pairs   keyword list    SVG attributes for the major tick lines
03140   """
03141   defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
03142   mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
03143 
03144   def __repr__(self):
03145     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)
03146 
03147   def __init__(self, xmin, xmax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
03148     self.xmin, self.xmax = xmin, xmax
03149 
03150     self.mini_attr = dict(self.mini_defaults)
03151     self.mini_attr.update(mini_attr)
03152 
03153     Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
03154 
03155     self.attr = dict(self.defaults)
03156     self.attr.update(attr)
03157 
03158   def SVG(self, trans=None):
03159     """Apply the transformation "trans" and return an SVG object."""
03160     self.last_ticks, self.last_miniticks = Ticks.interpret(self)
03161 
03162     ticksd = []
03163     for t in self.last_ticks.keys():
03164       ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
03165 
03166     miniticksd = []
03167     for t in self.last_miniticks:
03168       miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
03169 
03170     return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
03171     
03172 class VGrid(Ticks):
03173   """Draws the vertical lines of a grid over a specified region
03174   using the standard tick specification (see help(Ticks)) to place the
03175   grid lines.
03176 
03177   HGrid(ymin, ymax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
03178 
03179   ymin, ymax              required        the y range
03180   low, high               required        the x range
03181   ticks                   default=-10     request ticks according to the standard
03182                                           tick specification (see help(Ticks))
03183   miniticks               default=False   request miniticks according to the
03184                                           standard minitick specification
03185   logbase                 default=None    if a number, the axis is logarithmic
03186                                           with ticks at the given base (usually 10)
03187   mini_attr               default={}      SVG attributes for the minitick-lines
03188                                           (if miniticks != False)
03189   attribute=value pairs   keyword list    SVG attributes for the major tick lines
03190   """
03191   defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
03192   mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
03193 
03194   def __repr__(self):
03195     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)
03196 
03197   def __init__(self, ymin, ymax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
03198     self.ymin, self.ymax = ymin, ymax
03199 
03200     self.mini_attr = dict(self.mini_defaults)
03201     self.mini_attr.update(mini_attr)
03202 
03203     Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
03204 
03205     self.attr = dict(self.defaults)
03206     self.attr.update(attr)
03207 
03208   def SVG(self, trans=None):
03209     """Apply the transformation "trans" and return an SVG object."""
03210     self.last_ticks, self.last_miniticks = Ticks.interpret(self)
03211 
03212     ticksd = []
03213     for t in self.last_ticks.keys():
03214       ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
03215 
03216     miniticksd = []
03217     for t in self.last_miniticks:
03218       miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
03219 
03220     return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
03221 
03222 class Grid(Ticks):
03223   """Draws a grid over a specified region using the standard tick
03224   specification (see help(Ticks)) to place the grid lines.
03225 
03226   Grid(xmin, xmax, ymin, ymax, ticks, miniticks, logbase, mini_attr, attribute=value)
03227 
03228   xmin, xmax              required        the x range
03229   ymin, ymax              required        the y range
03230   ticks                   default=-10     request ticks according to the standard
03231                                           tick specification (see help(Ticks))
03232   miniticks               default=False   request miniticks according to the
03233                                           standard minitick specification
03234   logbase                 default=None    if a number, the axis is logarithmic
03235                                           with ticks at the given base (usually 10)
03236   mini_attr               default={}      SVG attributes for the minitick-lines
03237                                           (if miniticks != False)
03238   attribute=value pairs   keyword list    SVG attributes for the major tick lines
03239   """
03240   defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
03241   mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
03242 
03243   def __repr__(self):
03244     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)
03245 
03246   def __init__(self, xmin, xmax, ymin, ymax, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
03247     self.xmin, self.xmax = xmin, xmax
03248     self.ymin, self.ymax = ymin, ymax
03249 
03250     self.mini_attr = dict(self.mini_defaults)
03251     self.mini_attr.update(mini_attr)
03252 
03253     Ticks.__init__(self, None, None, None, ticks, miniticks, None, logbase)
03254 
03255     self.attr = dict(self.defaults)
03256     self.attr.update(attr)
03257 
03258   def SVG(self, trans=None):
03259     """Apply the transformation "trans" and return an SVG object."""
03260     self.low, self.high = self.xmin, self.xmax
03261     self.last_xticks, self.last_xminiticks = Ticks.interpret(self)
03262     self.low, self.high = self.ymin, self.ymax
03263     self.last_yticks, self.last_yminiticks = Ticks.interpret(self)
03264 
03265     ticksd = []
03266     for t in self.last_xticks.keys():
03267       ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
03268     for t in self.last_yticks.keys():
03269       ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
03270 
03271     miniticksd = []
03272     for t in self.last_xminiticks:
03273       miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
03274     for t in self.last_yminiticks:
03275       miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
03276 
03277     return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
03278 
03279 ######################################################################
03280 
03281 class XErrorBars:
03282   """Draws x error bars at a set of points. This is usually used
03283   before (under) a set of Dots at the same points.
03284 
03285   XErrorBars(d, attribute=value)
03286 
03287   d                       required        list of (x,y,xerr...) points
03288   attribute=value pairs   keyword list    SVG attributes
03289 
03290   If points in d have
03291 
03292       * 3 elements, the third is the symmetric error bar
03293       * 4 elements, the third and fourth are the asymmetric lower and
03294         upper error bar. The third element should be negative,
03295         e.g. (5, 5, -1, 2) is a bar from 4 to 7.
03296       * more than 4, a tick mark is placed at each value. This lets
03297         you nest errors from different sources, correlated and
03298         uncorrelated, statistical and systematic, etc.
03299   """
03300   defaults = {"stroke-width":"0.25pt"}
03301 
03302   def __repr__(self):
03303     return "<XErrorBars (%d nodes)>" % len(self.d)
03304 
03305   def __init__(self, d=[], **attr):
03306     self.d = list(d)
03307 
03308     self.attr = dict(self.defaults)
03309     self.attr.update(attr)
03310     
03311   def SVG(self, trans=None):
03312     """Apply the transformation "trans" and return an SVG object."""
03313     if isinstance(trans, basestring): trans = totrans(trans) # only once
03314 
03315     output = SVG("g")
03316     for p in self.d:
03317       x, y = p[0], p[1]
03318 
03319       if len(p) == 3: bars = [x - p[2], x + p[2]]
03320       else: bars = [x + pi for pi in p[2:]]
03321       
03322       start, end = min(bars), max(bars)
03323       output.append(LineAxis(start, y, end, y, start, end, bars, False, False, **self.attr).SVG(trans))
03324 
03325     return output
03326 
03327 class YErrorBars:
03328   """Draws y error bars at a set of points. This is usually used
03329   before (under) a set of Dots at the same points.
03330 
03331   YErrorBars(d, attribute=value)
03332 
03333   d                       required        list of (x,y,yerr...) points
03334   attribute=value pairs   keyword list    SVG attributes
03335 
03336   If points in d have
03337 
03338       * 3 elements, the third is the symmetric error bar
03339       * 4 elements, the third and fourth are the asymmetric lower and
03340         upper error bar. The third element should be negative,
03341         e.g. (5, 5, -1, 2) is a bar from 4 to 7.
03342       * more than 4, a tick mark is placed at each value. This lets
03343         you nest errors from different sources, correlated and
03344         uncorrelated, statistical and systematic, etc.
03345   """
03346   defaults = {"stroke-width":"0.25pt"}
03347 
03348   def __repr__(self):
03349     return "<YErrorBars (%d nodes)>" % len(self.d)
03350 
03351   def __init__(self, d=[], **attr):
03352     self.d = list(d)
03353 
03354     self.attr = dict(self.defaults)
03355     self.attr.update(attr)
03356     
03357   def SVG(self, trans=None):
03358     """Apply the transformation "trans" and return an SVG object."""
03359     if isinstance(trans, basestring): trans = totrans(trans) # only once
03360 
03361     output = SVG("g")
03362     for p in self.d:
03363       x, y = p[0], p[1]
03364 
03365       if len(p) == 3: bars = [y - p[2], y + p[2]]
03366       else: bars = [y + pi for pi in p[2:]]
03367       
03368       start, end = min(bars), max(bars)
03369       output.append(LineAxis(x, start, x, end, start, end, bars, False, False, **self.attr).SVG(trans))
03370 
03371     return output