Home | History | Annotate | Download | only in pattern_tools
      1 #!/usr/bin/env python
      2 
      3 # svgfig.py copyright (C) 2008 Jim Pivarski <jpivarski (at] gmail.com>
      4 #
      5 # This program is free software; you can redistribute it and/or
      6 # modify it under the terms of the GNU General Public License
      7 # as published by the Free Software Foundation; either version 2
      8 # of the License, or (at your option) any later version.
      9 #
     10 # This program is distributed in the hope that it will be useful,
     11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 # GNU General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU General Public License
     16 # along with this program; if not, write to the Free Software
     17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
     18 #
     19 # Full licence is in the file COPYING and at http://www.gnu.org/copyleft/gpl.html
     20 
     21 import re, codecs, os, platform, copy, itertools, math, cmath, random, sys, copy
     22 _epsilon = 1e-5
     23 
     24 
     25 if re.search("windows", platform.system(), re.I):
     26     try:
     27         import _winreg
     28         _default_directory = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER,
     29                              r"Software\Microsoft\Windows\Current Version\Explorer\Shell Folders"), "Desktop")[0]
     30 #   tmpdir = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Environment"), "TEMP")[0]
     31 #   if tmpdir[0:13] != "%USERPROFILE%":
     32 #     tmpdir = os.path.expanduser("~") + tmpdir[13:]
     33     except:
     34         _default_directory = os.path.expanduser("~") + os.sep + "Desktop"
     35 
     36 _default_fileName = "tmp.svg"
     37 
     38 _hacks = {}
     39 _hacks["inkscape-text-vertical-shift"] = False
     40 
     41 
     42 def rgb(r, g, b, maximum=1.):
     43     """Create an SVG color string "#xxyyzz" from r, g, and b.
     44 
     45     r,g,b = 0 is black and r,g,b = maximum is white.
     46     """
     47     return "#%02x%02x%02x" % (max(0, min(r*255./maximum, 255)),
     48                               max(0, min(g*255./maximum, 255)),
     49                               max(0, min(b*255./maximum, 255)))
     50 
     51 def attr_preprocess(attr):
     52     for name in attr.keys():
     53         name_colon = re.sub("__", ":", name)
     54         if name_colon != name:
     55             attr[name_colon] = attr[name]
     56             del attr[name]
     57             name = name_colon
     58 
     59         name_dash = re.sub("_", "-", name)
     60         if name_dash != name:
     61             attr[name_dash] = attr[name]
     62             del attr[name]
     63             name = name_dash
     64 
     65     return attr
     66 
     67 
     68 class SVG:
     69     """A tree representation of an SVG image or image fragment.
     70 
     71     SVG(t, sub, sub, sub..., attribute=value)
     72 
     73     t                       required             SVG type name
     74     sub                     optional list        nested SVG elements or text/Unicode
     75     attribute=value pairs   optional keywords    SVG attributes
     76 
     77     In attribute names, "__" becomes ":" and "_" becomes "-".
     78 
     79     SVG in XML
     80 
     81     <g id="mygroup" fill="blue">
     82         <rect x="1" y="1" width="2" height="2" />
     83         <rect x="3" y="3" width="2" height="2" />
     84     </g>
     85 
     86     SVG in Python
     87 
     88     >>> svg = SVG("g", SVG("rect", x=1, y=1, width=2, height=2), \
     89     ...                SVG("rect", x=3, y=3, width=2, height=2), \
     90     ...           id="mygroup", fill="blue")
     91 
     92     Sub-elements and attributes may be accessed through tree-indexing:
     93 
     94     >>> svg = SVG("text", SVG("tspan", "hello there"), stroke="none", fill="black")
     95     >>> svg[0]
     96     <tspan (1 sub) />
     97     >>> svg[0, 0]
     98     'hello there'
     99     >>> svg["fill"]
    100     'black'
    101 
    102     Iteration is depth-first:
    103 
    104     >>> svg = SVG("g", SVG("g", SVG("line", x1=0, y1=0, x2=1, y2=1)), \
    105     ...                SVG("text", SVG("tspan", "hello again")))
    106     ...
    107     >>> for ti, s in svg:
    108     ...     print ti, repr(s)
    109     ...
    110     (0,) <g (1 sub) />
    111     (0, 0) <line x2=1 y1=0 x1=0 y2=1 />
    112     (0, 0, 'x2') 1
    113     (0, 0, 'y1') 0
    114     (0, 0, 'x1') 0
    115     (0, 0, 'y2') 1
    116     (1,) <text (1 sub) />
    117     (1, 0) <tspan (1 sub) />
    118     (1, 0, 0) 'hello again'
    119 
    120     Use "print" to navigate:
    121 
    122     >>> print svg
    123     None                 <g (2 sub) />
    124     [0]                      <g (1 sub) />
    125     [0, 0]                       <line x2=1 y1=0 x1=0 y2=1 />
    126     [1]                      <text (1 sub) />
    127     [1, 0]                       <tspan (1 sub) />
    128     """
    129     def __init__(self, *t_sub, **attr):
    130         if len(t_sub) == 0:
    131             raise TypeError, "SVG element must have a t (SVG type)"
    132 
    133         # first argument is t (SVG type)
    134         self.t = t_sub[0]
    135         # the rest are sub-elements
    136         self.sub = list(t_sub[1:])
    137 
    138         # keyword arguments are attributes
    139         # need to preprocess to handle differences between SVG and Python syntax
    140         self.attr = attr_preprocess(attr)
    141 
    142     def __getitem__(self, ti):
    143         """Index is a list that descends tree, returning a sub-element if
    144         it ends with a number and an attribute if it ends with a string."""
    145         obj = self
    146         if isinstance(ti, (list, tuple)):
    147             for i in ti[:-1]:
    148                 obj = obj[i]
    149             ti = ti[-1]
    150 
    151         if isinstance(ti, (int, long, slice)):
    152             return obj.sub[ti]
    153         else:
    154             return obj.attr[ti]
    155 
    156     def __setitem__(self, ti, value):
    157         """Index is a list that descends tree, returning a sub-element if
    158         it ends with a number and an attribute if it ends with a string."""
    159         obj = self
    160         if isinstance(ti, (list, tuple)):
    161             for i in ti[:-1]:
    162                 obj = obj[i]
    163             ti = ti[-1]
    164 
    165         if isinstance(ti, (int, long, slice)):
    166             obj.sub[ti] = value
    167         else:
    168             obj.attr[ti] = value
    169 
    170     def __delitem__(self, ti):
    171         """Index is a list that descends tree, returning a sub-element if
    172         it ends with a number and an attribute if it ends with a string."""
    173         obj = self
    174         if isinstance(ti, (list, tuple)):
    175             for i in ti[:-1]:
    176                 obj = obj[i]
    177             ti = ti[-1]
    178 
    179         if isinstance(ti, (int, long, slice)):
    180             del obj.sub[ti]
    181         else:
    182             del obj.attr[ti]
    183 
    184     def __contains__(self, value):
    185         """x in svg == True iff x is an attribute in svg."""
    186         return value in self.attr
    187 
    188     def __eq__(self, other):
    189         """x == y iff x represents the same SVG as y."""
    190         if id(self) == id(other):
    191             return True
    192         return (isinstance(other, SVG) and
    193                 self.t == other.t and self.sub == other.sub and self.attr == other.attr)
    194 
    195     def __ne__(self, other):
    196         """x != y iff x does not represent the same SVG as y."""
    197         return not (self == other)
    198 
    199     def append(self, x):
    200         """Appends x to the list of sub-elements (drawn last, overlaps
    201         other primitives)."""
    202         self.sub.append(x)
    203 
    204     def prepend(self, x):
    205         """Prepends x to the list of sub-elements (drawn first may be
    206         overlapped by other primitives)."""
    207         self.sub[0:0] = [x]
    208 
    209     def extend(self, x):
    210         """Extends list of sub-elements by a list x."""
    211         self.sub.extend(x)
    212 
    213     def clone(self, shallow=False):
    214         """Deep copy of SVG tree.  Set shallow=True for a shallow copy."""
    215         if shallow:
    216             return copy.copy(self)
    217         else:
    218             return copy.deepcopy(self)
    219 
    220     ### nested class
    221     class SVGDepthIterator:
    222         """Manages SVG iteration."""
    223 
    224         def __init__(self, svg, ti, depth_limit):
    225             self.svg = svg
    226             self.ti = ti
    227             self.shown = False
    228             self.depth_limit = depth_limit
    229 
    230         def __iter__(self):
    231             return self
    232 
    233         def next(self):
    234             if not self.shown:
    235                 self.shown = True
    236                 if self.ti != ():
    237                     return self.ti, self.svg
    238 
    239             if not isinstance(self.svg, SVG):
    240                 raise StopIteration
    241             if self.depth_limit is not None and len(self.ti) >= self.depth_limit:
    242                 raise StopIteration
    243 
    244             if "iterators" not in self.__dict__:
    245                 self.iterators = []
    246                 for i, s in enumerate(self.svg.sub):
    247                     self.iterators.append(self.__class__(s, self.ti + (i,), self.depth_limit))
    248                 for k, s in self.svg.attr.items():
    249                     self.iterators.append(self.__class__(s, self.ti + (k,), self.depth_limit))
    250                 self.iterators = itertools.chain(*self.iterators)
    251 
    252             return self.iterators.next()
    253     ### end nested class
    254 
    255     def depth_first(self, depth_limit=None):
    256         """Returns a depth-first generator over the SVG.  If depth_limit
    257         is a number, stop recursion at that depth."""
    258         return self.SVGDepthIterator(self, (), depth_limit)
    259 
    260     def breadth_first(self, depth_limit=None):
    261         """Not implemented yet.  Any ideas on how to do it?
    262 
    263         Returns a breadth-first generator over the SVG.  If depth_limit
    264         is a number, stop recursion at that depth."""
    265         raise NotImplementedError, "Got an algorithm for breadth-first searching a tree without effectively copying the tree?"
    266 
    267     def __iter__(self):
    268         return self.depth_first()
    269 
    270     def items(self, sub=True, attr=True, text=True):
    271         """Get a recursively-generated list of tree-index, sub-element/attribute pairs.
    272 
    273         If sub == False, do not show sub-elements.
    274         If attr == False, do not show attributes.
    275         If text == False, do not show text/Unicode sub-elements.
    276         """
    277         output = []
    278         for ti, s in self:
    279             show = False
    280             if isinstance(ti[-1], (int, long)):
    281                 if isinstance(s, basestring):
    282                     show = text
    283                 else:
    284                     show = sub
    285             else:
    286                 show = attr
    287 
    288             if show:
    289                 output.append((ti, s))
    290         return output
    291 
    292     def keys(self, sub=True, attr=True, text=True):
    293         """Get a recursively-generated list of tree-indexes.
    294 
    295         If sub == False, do not show sub-elements.
    296         If attr == False, do not show attributes.
    297         If text == False, do not show text/Unicode sub-elements.
    298         """
    299         return [ti for ti, s in self.items(sub, attr, text)]
    300 
    301     def values(self, sub=True, attr=True, text=True):
    302         """Get a recursively-generated list of sub-elements and attributes.
    303 
    304         If sub == False, do not show sub-elements.
    305         If attr == False, do not show attributes.
    306         If text == False, do not show text/Unicode sub-elements.
    307         """
    308         return [s for ti, s in self.items(sub, attr, text)]
    309 
    310     def __repr__(self):
    311         return self.xml(depth_limit=0)
    312 
    313     def __str__(self):
    314         """Print (actually, return a string of) the tree in a form useful for browsing."""
    315         return self.tree(sub=True, attr=False, text=False)
    316 
    317     def tree(self, depth_limit=None, sub=True, attr=True, text=True, tree_width=20, obj_width=80):
    318         """Print (actually, return a string of) the tree in a form useful for browsing.
    319 
    320         If depth_limit == a number, stop recursion at that depth.
    321         If sub == False, do not show sub-elements.
    322         If attr == False, do not show attributes.
    323         If text == False, do not show text/Unicode sub-elements.
    324         tree_width is the number of characters reserved for printing tree indexes.
    325         obj_width is the number of characters reserved for printing sub-elements/attributes.
    326         """
    327         output = []
    328 
    329         line = "%s %s" % (("%%-%ds" % tree_width) % repr(None),
    330                           ("%%-%ds" % obj_width) % (repr(self))[0:obj_width])
    331         output.append(line)
    332 
    333         for ti, s in self.depth_first(depth_limit):
    334             show = False
    335             if isinstance(ti[-1], (int, long)):
    336                 if isinstance(s, basestring):
    337                     show = text
    338                 else:
    339                     show = sub
    340             else:
    341                 show = attr
    342 
    343             if show:
    344                 line = "%s %s" % (("%%-%ds" % tree_width) % repr(list(ti)),
    345                                   ("%%-%ds" % obj_width) % ("    "*len(ti) + repr(s))[0:obj_width])
    346                 output.append(line)
    347 
    348         return "\n".join(output)
    349 
    350     def xml(self, indent=u"    ", newl=u"\n", depth_limit=None, depth=0):
    351         """Get an XML representation of the SVG.
    352 
    353         indent      string used for indenting
    354         newl        string used for newlines
    355         If depth_limit == a number, stop recursion at that depth.
    356         depth       starting depth (not useful for users)
    357 
    358         print svg.xml()
    359         """
    360         attrstr = []
    361         for n, v in self.attr.items():
    362             if isinstance(v, dict):
    363                 v = u"; ".join([u"%s:%s" % (ni, vi) for ni, vi in v.items()])
    364             elif isinstance(v, (list, tuple)):
    365                 v = u", ".join(v)
    366             attrstr.append(u" %s=%s" % (n, repr(v)))
    367         attrstr = u"".join(attrstr)
    368 
    369         if len(self.sub) == 0:
    370             return u"%s<%s%s />" % (indent * depth, self.t, attrstr)
    371 
    372         if depth_limit is None or depth_limit > depth:
    373             substr = []
    374             for s in self.sub:
    375                 if isinstance(s, SVG):
    376                     substr.append(s.xml(indent, newl, depth_limit, depth + 1) + newl)
    377                 elif isinstance(s, basestring):
    378                     substr.append(u"%s%s%s" % (indent * (depth + 1), s, newl))
    379                 else:
    380                     substr.append("%s%s%s" % (indent * (depth + 1), repr(s), newl))
    381             substr = u"".join(substr)
    382 
    383             return u"%s<%s%s>%s%s%s</%s>" % (indent * depth, self.t, attrstr, newl, substr, indent * depth, self.t)
    384 
    385         else:
    386             return u"%s<%s (%d sub)%s />" % (indent * depth, self.t, len(self.sub), attrstr)
    387 
    388     def standalone_xml(self, indent=u"    ", newl=u"\n", encoding=u"utf-8"):
    389         """Get an XML representation of the SVG that can be saved/rendered.
    390 
    391         indent      string used for indenting
    392         newl        string used for newlines
    393         """
    394 
    395         if self.t == "svg":
    396             top = self
    397         else:
    398             top = canvas(self)
    399         return u"""\
    400 <?xml version="1.0" encoding="%s" standalone="no"?>
    401 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
    402 
    403 """ % encoding + (u"".join(top.__standalone_xml(indent, newl)))  # end of return statement
    404 
    405     def __standalone_xml(self, indent, newl):
    406         output = [u"<%s" % self.t]
    407 
    408         for n, v in self.attr.items():
    409             if isinstance(v, dict):
    410                 v = u"; ".join([u"%s:%s" % (ni, vi) for ni, vi in v.items()])
    411             elif isinstance(v, (list, tuple)):
    412                 v = u", ".join(v)
    413             output.append(u' %s="%s"' % (n, v))
    414 
    415         if len(self.sub) == 0:
    416             output.append(u" />%s%s" % (newl, newl))
    417             return output
    418 
    419         elif self.t == "text" or self.t == "tspan" or self.t == "style":
    420             output.append(u">")
    421 
    422         else:
    423             output.append(u">%s%s" % (newl, newl))
    424 
    425         for s in self.sub:
    426             if isinstance(s, SVG):
    427                 output.extend(s.__standalone_xml(indent, newl))
    428             else:
    429                 output.append(unicode(s))
    430 
    431         if self.t == "tspan":
    432             output.append(u"</%s>" % self.t)
    433         else:
    434             output.append(u"</%s>%s%s" % (self.t, newl, newl))
    435 
    436         return output
    437 
    438     def interpret_fileName(self, fileName=None):
    439         if fileName is None:
    440             fileName = _default_fileName
    441         if re.search("windows", platform.system(), re.I) and not os.path.isabs(fileName):
    442             fileName = _default_directory + os.sep + fileName
    443         return fileName
    444 
    445     def save(self, fileName=None, encoding="utf-8", compresslevel=None):
    446         """Save to a file for viewing.  Note that svg.save() overwrites the file named _default_fileName.
    447 
    448         fileName        default=None            note that _default_fileName will be overwritten if
    449                                                 no fileName is specified. If the extension
    450                                                 is ".svgz" or ".gz", the output will be gzipped
    451         encoding        default="utf-8"         file encoding
    452         compresslevel   default=None            if a number, the output will be gzipped with that
    453                                                 compression level (1-9, 1 being fastest and 9 most
    454                                                 thorough)
    455         """
    456         fileName = self.interpret_fileName(fileName)
    457 
    458         if compresslevel is not None or re.search(r"\.svgz$", fileName, re.I) or re.search(r"\.gz$", fileName, re.I):
    459             import gzip
    460             if compresslevel is None:
    461                 f = gzip.GzipFile(fileName, "w")
    462             else:
    463                 f = gzip.GzipFile(fileName, "w", compresslevel)
    464 
    465             f = codecs.EncodedFile(f, "utf-8", encoding)
    466             f.write(self.standalone_xml(encoding=encoding))
    467             f.close()
    468 
    469         else:
    470             f = codecs.open(fileName, "w", encoding=encoding)
    471             f.write(self.standalone_xml(encoding=encoding))
    472             f.close()
    473 
    474     def inkview(self, fileName=None, encoding="utf-8"):
    475         """View in "inkview", assuming that program is available on your system.
    476 
    477         fileName        default=None            note that any file named _default_fileName will be
    478                                                 overwritten if no fileName is specified. If the extension
    479                                                 is ".svgz" or ".gz", the output will be gzipped
    480         encoding        default="utf-8"         file encoding
    481         """
    482         fileName = self.interpret_fileName(fileName)
    483         self.save(fileName, encoding)
    484         os.spawnvp(os.P_NOWAIT, "inkview", ("inkview", fileName))
    485 
    486     def inkscape(self, fileName=None, encoding="utf-8"):
    487         """View in "inkscape", assuming that program is available on your system.
    488 
    489         fileName        default=None            note that any file named _default_fileName will be
    490                                                 overwritten if no fileName is specified. If the extension
    491                                                 is ".svgz" or ".gz", the output will be gzipped
    492         encoding        default="utf-8"         file encoding
    493         """
    494         fileName = self.interpret_fileName(fileName)
    495         self.save(fileName, encoding)
    496         os.spawnvp(os.P_NOWAIT, "inkscape", ("inkscape", fileName))
    497 
    498     def firefox(self, fileName=None, encoding="utf-8"):
    499         """View in "firefox", assuming that program is available on your system.
    500 
    501         fileName        default=None            note that any file named _default_fileName will be
    502                                                 overwritten if no fileName is specified. If the extension
    503                                                 is ".svgz" or ".gz", the output will be gzipped
    504         encoding        default="utf-8"         file encoding
    505         """
    506         fileName = self.interpret_fileName(fileName)
    507         self.save(fileName, encoding)
    508         os.spawnvp(os.P_NOWAIT, "firefox", ("firefox", fileName))
    509 
    510 ######################################################################
    511 
    512 _canvas_defaults = {"width": "400px",
    513                     "height": "400px",
    514                     "viewBox": "0 0 100 100",
    515                     "xmlns": "http://www.w3.org/2000/svg",
    516                     "xmlns:xlink": "http://www.w3.org/1999/xlink",
    517                     "version": "1.1",
    518                     "style": {"stroke": "black",
    519                               "fill": "none",
    520                               "stroke-width": "0.5pt",
    521                               "stroke-linejoin": "round",
    522                               "text-anchor": "middle",
    523                              },
    524                     "font-family": ["Helvetica", "Arial", "FreeSans", "Sans", "sans", "sans-serif"],
    525                    }
    526 
    527 def canvas(*sub, **attr):
    528     """Creates a top-level SVG object, allowing the user to control the
    529     image size and aspect ratio.
    530 
    531     canvas(sub, sub, sub..., attribute=value)
    532 
    533     sub                     optional list       nested SVG elements or text/Unicode
    534     attribute=value pairs   optional keywords   SVG attributes
    535 
    536     Default attribute values:
    537 
    538     width           "400px"
    539     height          "400px"
    540     viewBox         "0 0 100 100"
    541     xmlns           "http://www.w3.org/2000/svg"
    542     xmlns:xlink     "http://www.w3.org/1999/xlink"
    543     version         "1.1"
    544     style           "stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoin:round; text-anchor:middle"
    545     font-family     "Helvetica,Arial,FreeSans?,Sans,sans,sans-serif"
    546     """
    547     attributes = dict(_canvas_defaults)
    548     attributes.update(attr)
    549 
    550     if sub is None or sub == ():
    551         return SVG("svg", **attributes)
    552     else:
    553         return SVG("svg", *sub, **attributes)
    554 
    555 def canvas_outline(*sub, **attr):
    556     """Same as canvas(), but draws an outline around the drawable area,
    557     so that you know how close your image is to the edges."""
    558     svg = canvas(*sub, **attr)
    559     match = re.match(r"[, \t]*([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]*", svg["viewBox"])
    560     if match is None:
    561         raise ValueError, "canvas viewBox is incorrectly formatted"
    562     x, y, width, height = [float(x) for x in match.groups()]
    563     svg.prepend(SVG("rect", x=x, y=y, width=width, height=height, stroke="none", fill="cornsilk"))
    564     svg.append(SVG("rect", x=x, y=y, width=width, height=height, stroke="black", fill="none"))
    565     return svg
    566 
    567 def template(fileName, svg, replaceme="REPLACEME"):
    568     """Loads an SVG image from a file, replacing instances of
    569     <REPLACEME /> with a given svg object.
    570 
    571     fileName         required                name of the template SVG
    572     svg              required                SVG object for replacement
    573     replaceme        default="REPLACEME"     fake SVG element to be replaced by the given object
    574 
    575     >>> print load("template.svg")
    576     None                 <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
    577     [0]                      <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
    578     [1]                      <REPLACEME />
    579     >>>
    580     >>> print template("template.svg", SVG("circle", cx=50, cy=50, r=30))
    581     None                 <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
    582     [0]                      <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
    583     [1]                      <circle cy=50 cx=50 r=30 />
    584     """
    585     output = load(fileName)
    586     for ti, s in output:
    587         if isinstance(s, SVG) and s.t == replaceme:
    588             output[ti] = svg
    589     return output
    590 
    591 ######################################################################
    592 
    593 def load(fileName):
    594     """Loads an SVG image from a file."""
    595     return load_stream(file(fileName))
    596 
    597 def load_stream(stream):
    598     """Loads an SVG image from a stream (can be a string or a file object)."""
    599 
    600     from xml.sax import handler, make_parser
    601     from xml.sax.handler import feature_namespaces, feature_external_ges, feature_external_pes
    602 
    603     class ContentHandler(handler.ContentHandler):
    604         def __init__(self):
    605             self.stack = []
    606             self.output = None
    607             self.all_whitespace = re.compile(r"^\s*$")
    608 
    609         def startElement(self, name, attr):
    610             s = SVG(name)
    611             s.attr = dict(attr.items())
    612             if len(self.stack) > 0:
    613                 last = self.stack[-1]
    614                 last.sub.append(s)
    615             self.stack.append(s)
    616 
    617         def characters(self, ch):
    618             if not isinstance(ch, basestring) or self.all_whitespace.match(ch) is None:
    619                 if len(self.stack) > 0:
    620                     last = self.stack[-1]
    621                     if len(last.sub) > 0 and isinstance(last.sub[-1], basestring):
    622                         last.sub[-1] = last.sub[-1] + "\n" + ch
    623                     else:
    624                         last.sub.append(ch)
    625 
    626         def endElement(self, name):
    627             if len(self.stack) > 0:
    628                 last = self.stack[-1]
    629                 if (isinstance(last, SVG) and last.t == "style" and
    630                     "type" in last.attr and last.attr["type"] == "text/css" and
    631                     len(last.sub) == 1 and isinstance(last.sub[0], basestring)):
    632                     last.sub[0] = "<![CDATA[\n" + last.sub[0] + "]]>"
    633 
    634             self.output = self.stack.pop()
    635 
    636     ch = ContentHandler()
    637     parser = make_parser()
    638     parser.setContentHandler(ch)
    639     parser.setFeature(feature_namespaces, 0)
    640     parser.setFeature(feature_external_ges, 0)
    641     parser.parse(stream)
    642     return ch.output
    643 
    644 ######################################################################
    645 def set_func_name(f, name):
    646     """try to patch the function name string into a function object"""
    647     try:
    648         f.func_name = name
    649     except TypeError:
    650         # py 2.3 raises: TypeError: readonly attribute
    651         pass
    652 
    653 def totrans(expr, vars=("x", "y"), globals=None, locals=None):
    654     """Converts to a coordinate transformation (a function that accepts
    655     two arguments and returns two values).
    656 
    657     expr       required                  a string expression or a function
    658                                          of two real or one complex value
    659     vars       default=("x", "y")        independent variable names; a singleton
    660                                          ("z",) is interpreted as complex
    661     globals    default=None              dict of global variables
    662     locals     default=None              dict of local variables
    663     """
    664     if locals is None:
    665         locals = {}  # python 2.3's eval() won't accept None
    666 
    667     if callable(expr):
    668         if expr.func_code.co_argcount == 2:
    669             return expr
    670 
    671         elif expr.func_code.co_argcount == 1:
    672             split = lambda z: (z.real, z.imag)
    673             output = lambda x, y: split(expr(x + y*1j))
    674             set_func_name(output, expr.func_name)
    675             return output
    676 
    677         else:
    678             raise TypeError, "must be a function of 2 or 1 variables"
    679 
    680     if len(vars) == 2:
    681         g = math.__dict__
    682         if globals is not None:
    683             g.update(globals)
    684         output = eval("lambda %s, %s: (%s)" % (vars[0], vars[1], expr), g, locals)
    685         set_func_name(output, "%s,%s -> %s" % (vars[0], vars[1], expr))
    686         return output
    687 
    688     elif len(vars) == 1:
    689         g = cmath.__dict__
    690         if globals is not None:
    691             g.update(globals)
    692         output = eval("lambda %s: (%s)" % (vars[0], expr), g, locals)
    693         split = lambda z: (z.real, z.imag)
    694         output2 = lambda x, y: split(output(x + y*1j))
    695         set_func_name(output2, "%s -> %s" % (vars[0], expr))
    696         return output2
    697 
    698     else:
    699         raise TypeError, "vars must have 2 or 1 elements"
    700 
    701 
    702 def window(xmin, xmax, ymin, ymax, x=0, y=0, width=100, height=100,
    703            xlogbase=None, ylogbase=None, minusInfinity=-1000, flipx=False, flipy=True):
    704     """Creates and returns a coordinate transformation (a function that
    705     accepts two arguments and returns two values) that transforms from
    706         (xmin, ymin), (xmax, ymax)
    707     to
    708         (x, y), (x + width, y + height).
    709 
    710     xlogbase, ylogbase    default=None, None     if a number, transform
    711                                                  logarithmically with given base
    712     minusInfinity         default=-1000          what to return if
    713                                                  log(0 or negative) is attempted
    714     flipx                 default=False          if true, reverse the direction of x
    715     flipy                 default=True           if true, reverse the direction of y
    716 
    717     (When composing windows, be sure to set flipy=False.)
    718     """
    719 
    720     if flipx:
    721         ox1 = x + width
    722         ox2 = x
    723     else:
    724         ox1 = x
    725         ox2 = x + width
    726     if flipy:
    727         oy1 = y + height
    728         oy2 = y
    729     else:
    730         oy1 = y
    731         oy2 = y + height
    732     ix1 = xmin
    733     iy1 = ymin
    734     ix2 = xmax
    735     iy2 = ymax
    736 
    737     if xlogbase is not None and (ix1 <= 0. or ix2 <= 0.):
    738         raise ValueError, "x range incompatible with log scaling: (%g, %g)" % (ix1, ix2)
    739 
    740     if ylogbase is not None and (iy1 <= 0. or iy2 <= 0.):
    741         raise ValueError, "y range incompatible with log scaling: (%g, %g)" % (iy1, iy2)
    742 
    743     def maybelog(t, it1, it2, ot1, ot2, logbase):
    744         if t <= 0.:
    745             return minusInfinity
    746         else:
    747             return ot1 + 1.*(math.log(t, logbase) - math.log(it1, logbase))/(math.log(it2, logbase) - math.log(it1, logbase)) * (ot2 - ot1)
    748 
    749     xlogstr, ylogstr = "", ""
    750 
    751     if xlogbase is None:
    752         xfunc = lambda x: ox1 + 1.*(x - ix1)/(ix2 - ix1) * (ox2 - ox1)
    753     else:
    754         xfunc = lambda x: maybelog(x, ix1, ix2, ox1, ox2, xlogbase)
    755         xlogstr = " xlog=%g" % xlogbase
    756 
    757     if ylogbase is None:
    758         yfunc = lambda y: oy1 + 1.*(y - iy1)/(iy2 - iy1) * (oy2 - oy1)
    759     else:
    760         yfunc = lambda y: maybelog(y, iy1, iy2, oy1, oy2, ylogbase)
    761         ylogstr = " ylog=%g" % ylogbase
    762 
    763     output = lambda x, y: (xfunc(x), yfunc(y))
    764 
    765     set_func_name(output, "(%g, %g), (%g, %g) -> (%g, %g), (%g, %g)%s%s" % (
    766                           ix1, ix2, iy1, iy2, ox1, ox2, oy1, oy2, xlogstr, ylogstr))
    767     return output
    768 
    769 
    770 def rotate(angle, cx=0, cy=0):
    771     """Creates and returns a coordinate transformation which rotates
    772     around (cx,cy) by "angle" degrees."""
    773     angle *= math.pi/180.
    774     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))
    775 
    776 
    777 class Fig:
    778     """Stores graphics primitive objects and applies a single coordinate
    779     transformation to them. To compose coordinate systems, nest Fig
    780     objects.
    781 
    782     Fig(obj, obj, obj..., trans=function)
    783 
    784     obj     optional list    a list of drawing primitives
    785     trans   default=None     a coordinate transformation function
    786 
    787     >>> fig = Fig(Line(0,0,1,1), Rect(0.2,0.2,0.8,0.8), trans="2*x, 2*y")
    788     >>> print fig.SVG().xml()
    789     <g>
    790         <path d='M0 0L2 2' />
    791         <path d='M0.4 0.4L1.6 0.4ZL1.6 1.6ZL0.4 1.6ZL0.4 0.4ZZ' />
    792     </g>
    793     >>> print Fig(fig, trans="x/2., y/2.").SVG().xml()
    794     <g>
    795         <path d='M0 0L1 1' />
    796         <path d='M0.2 0.2L0.8 0.2ZL0.8 0.8ZL0.2 0.8ZL0.2 0.2ZZ' />
    797     </g>
    798     """
    799 
    800     def __repr__(self):
    801         if self.trans is None:
    802             return "<Fig (%d items)>" % len(self.d)
    803         elif isinstance(self.trans, basestring):
    804             return "<Fig (%d items) x,y -> %s>" % (len(self.d), self.trans)
    805         else:
    806             return "<Fig (%d items) %s>" % (len(self.d), self.trans.func_name)
    807 
    808     def __init__(self, *d, **kwds):
    809         self.d = list(d)
    810         defaults = {"trans": None, }
    811         defaults.update(kwds)
    812         kwds = defaults
    813 
    814         self.trans = kwds["trans"]; del kwds["trans"]
    815         if len(kwds) != 0:
    816             raise TypeError, "Fig() got unexpected keyword arguments %s" % kwds.keys()
    817 
    818     def SVG(self, trans=None):
    819         """Apply the transformation "trans" and return an SVG object.
    820 
    821         Coordinate transformations in nested Figs will be composed.
    822         """
    823 
    824         if trans is None:
    825             trans = self.trans
    826         if isinstance(trans, basestring):
    827             trans = totrans(trans)
    828 
    829         output = SVG("g")
    830         for s in self.d:
    831             if isinstance(s, SVG):
    832                 output.append(s)
    833 
    834             elif isinstance(s, Fig):
    835                 strans = s.trans
    836                 if isinstance(strans, basestring):
    837                     strans = totrans(strans)
    838 
    839                 if trans is None:
    840                     subtrans = strans
    841                 elif strans is None:
    842                     subtrans = trans
    843                 else:
    844                     subtrans = lambda x, y: trans(*strans(x, y))
    845 
    846                 output.sub += s.SVG(subtrans).sub
    847 
    848             elif s is None:
    849                 pass
    850 
    851             else:
    852                 output.append(s.SVG(trans))
    853 
    854         return output
    855 
    856 
    857 class Plot:
    858     """Acts like Fig, but draws a coordinate axis. You also need to supply plot ranges.
    859 
    860     Plot(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
    861 
    862     xmin, xmax      required        minimum and maximum x values (in the objs' coordinates)
    863     ymin, ymax      required        minimum and maximum y values (in the objs' coordinates)
    864     obj             optional list   drawing primitives
    865     keyword options keyword list    options defined below
    866 
    867     The following are keyword options, with their default values:
    868 
    869     trans           None          transformation function
    870     x, y            5, 5          upper-left corner of the Plot in SVG coordinates
    871     width, height   90, 90        width and height of the Plot in SVG coordinates
    872     flipx, flipy    False, True   flip the sign of the coordinate axis
    873     minusInfinity   -1000         if an axis is logarithmic and an object is plotted at 0 or
    874                                   a negative value, -1000 will be used as a stand-in for NaN
    875     atx, aty        0, 0          the place where the coordinate axes cross
    876     xticks          -10           request ticks according to the standard tick specification
    877                                   (see help(Ticks))
    878     xminiticks      True          request miniticks according to the standard minitick
    879                                   specification
    880     xlabels         True          request tick labels according to the standard tick label
    881                                   specification
    882     xlogbase        None          if a number, the axis and transformation are logarithmic
    883                                   with ticks at the given base (10 being the most common)
    884     (same for y)
    885     arrows          None          if a new identifier, create arrow markers and draw them
    886                                   at the ends of the coordinate axes
    887     text_attr       {}            a dictionary of attributes for label text
    888     axis_attr       {}            a dictionary of attributes for the axis lines
    889     """
    890 
    891     def __repr__(self):
    892         if self.trans is None:
    893             return "<Plot (%d items)>" % len(self.d)
    894         else:
    895             return "<Plot (%d items) %s>" % (len(self.d), self.trans.func_name)
    896 
    897     def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
    898         self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
    899         self.d = list(d)
    900         defaults = {"trans": None,
    901                     "x": 5, "y": 5, "width": 90, "height": 90,
    902                     "flipx": False, "flipy": True,
    903                     "minusInfinity": -1000,
    904                     "atx": 0, "xticks": -10, "xminiticks": True, "xlabels": True, "xlogbase": None,
    905                     "aty": 0, "yticks": -10, "yminiticks": True, "ylabels": True, "ylogbase": None,
    906                     "arrows": None,
    907                     "text_attr": {}, "axis_attr": {},
    908                    }
    909         defaults.update(kwds)
    910         kwds = defaults
    911 
    912         self.trans = kwds["trans"]; del kwds["trans"]
    913         self.x = kwds["x"]; del kwds["x"]
    914         self.y = kwds["y"]; del kwds["y"]
    915         self.width = kwds["width"]; del kwds["width"]
    916         self.height = kwds["height"]; del kwds["height"]
    917         self.flipx = kwds["flipx"]; del kwds["flipx"]
    918         self.flipy = kwds["flipy"]; del kwds["flipy"]
    919         self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
    920         self.atx = kwds["atx"]; del kwds["atx"]
    921         self.xticks = kwds["xticks"]; del kwds["xticks"]
    922         self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
    923         self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
    924         self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
    925         self.aty = kwds["aty"]; del kwds["aty"]
    926         self.yticks = kwds["yticks"]; del kwds["yticks"]
    927         self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
    928         self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
    929         self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
    930         self.arrows = kwds["arrows"]; del kwds["arrows"]
    931         self.text_attr = kwds["text_attr"]; del kwds["text_attr"]
    932         self.axis_attr = kwds["axis_attr"]; del kwds["axis_attr"]
    933         if len(kwds) != 0:
    934             raise TypeError, "Plot() got unexpected keyword arguments %s" % kwds.keys()
    935 
    936     def SVG(self, trans=None):
    937         """Apply the transformation "trans" and return an SVG object."""
    938         if trans is None:
    939             trans = self.trans
    940         if isinstance(trans, basestring):
    941             trans = totrans(trans)
    942 
    943         self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax,
    944                                   x=self.x, y=self.y, width=self.width, height=self.height,
    945                                   xlogbase=self.xlogbase, ylogbase=self.ylogbase,
    946                                   minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
    947 
    948         d = ([Axes(self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty,
    949                    self.xticks, self.xminiticks, self.xlabels, self.xlogbase,
    950                    self.yticks, self.yminiticks, self.ylabels, self.ylogbase,
    951                    self.arrows, self.text_attr, **self.axis_attr)]
    952              + self.d)
    953 
    954         return Fig(Fig(*d, **{"trans": trans})).SVG(self.last_window)
    955 
    956 
    957 class Frame:
    958     text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
    959     axis_defaults = {}
    960 
    961     tick_length = 1.5
    962     minitick_length = 0.75
    963     text_xaxis_offset = 1.
    964     text_yaxis_offset = 2.
    965     text_xtitle_offset = 6.
    966     text_ytitle_offset = 12.
    967 
    968     def __repr__(self):
    969         return "<Frame (%d items)>" % len(self.d)
    970 
    971     def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
    972         """Acts like Fig, but draws a coordinate frame around the data. You also need to supply plot ranges.
    973 
    974         Frame(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
    975 
    976         xmin, xmax      required        minimum and maximum x values (in the objs' coordinates)
    977         ymin, ymax      required        minimum and maximum y values (in the objs' coordinates)
    978         obj             optional list   drawing primitives
    979         keyword options keyword list    options defined below
    980 
    981         The following are keyword options, with their default values:
    982 
    983         x, y            20, 5         upper-left corner of the Frame in SVG coordinates
    984         width, height   75, 80        width and height of the Frame in SVG coordinates
    985         flipx, flipy    False, True   flip the sign of the coordinate axis
    986         minusInfinity   -1000         if an axis is logarithmic and an object is plotted at 0 or
    987                                       a negative value, -1000 will be used as a stand-in for NaN
    988         xtitle          None          if a string, label the x axis
    989         xticks          -10           request ticks according to the standard tick specification
    990                                       (see help(Ticks))
    991         xminiticks      True          request miniticks according to the standard minitick
    992                                       specification
    993         xlabels         True          request tick labels according to the standard tick label
    994                                       specification
    995         xlogbase        None          if a number, the axis and transformation are logarithmic
    996                                       with ticks at the given base (10 being the most common)
    997         (same for y)
    998         text_attr       {}            a dictionary of attributes for label text
    999         axis_attr       {}            a dictionary of attributes for the axis lines
   1000         """
   1001 
   1002         self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
   1003         self.d = list(d)
   1004         defaults = {"x": 20, "y": 5, "width": 75, "height": 80,
   1005                     "flipx": False, "flipy": True, "minusInfinity": -1000,
   1006                     "xtitle": None, "xticks": -10, "xminiticks": True, "xlabels": True,
   1007                     "x2labels": None, "xlogbase": None,
   1008                     "ytitle": None, "yticks": -10, "yminiticks": True, "ylabels": True,
   1009                     "y2labels": None, "ylogbase": None,
   1010                     "text_attr": {}, "axis_attr": {},
   1011                    }
   1012         defaults.update(kwds)
   1013         kwds = defaults
   1014 
   1015         self.x = kwds["x"]; del kwds["x"]
   1016         self.y = kwds["y"]; del kwds["y"]
   1017         self.width = kwds["width"]; del kwds["width"]
   1018         self.height = kwds["height"]; del kwds["height"]
   1019         self.flipx = kwds["flipx"]; del kwds["flipx"]
   1020         self.flipy = kwds["flipy"]; del kwds["flipy"]
   1021         self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
   1022         self.xtitle = kwds["xtitle"]; del kwds["xtitle"]
   1023         self.xticks = kwds["xticks"]; del kwds["xticks"]
   1024         self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
   1025         self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
   1026         self.x2labels = kwds["x2labels"]; del kwds["x2labels"]
   1027         self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
   1028         self.ytitle = kwds["ytitle"]; del kwds["ytitle"]
   1029         self.yticks = kwds["yticks"]; del kwds["yticks"]
   1030         self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
   1031         self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
   1032         self.y2labels = kwds["y2labels"]; del kwds["y2labels"]
   1033         self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
   1034 
   1035         self.text_attr = dict(self.text_defaults)
   1036         self.text_attr.update(kwds["text_attr"]); del kwds["text_attr"]
   1037 
   1038         self.axis_attr = dict(self.axis_defaults)
   1039         self.axis_attr.update(kwds["axis_attr"]); del kwds["axis_attr"]
   1040 
   1041         if len(kwds) != 0:
   1042             raise TypeError, "Frame() got unexpected keyword arguments %s" % kwds.keys()
   1043 
   1044     def SVG(self):
   1045         """Apply the window transformation and return an SVG object."""
   1046 
   1047         self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax,
   1048                                   x=self.x, y=self.y, width=self.width, height=self.height,
   1049                                   xlogbase=self.xlogbase, ylogbase=self.ylogbase,
   1050                                   minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
   1051 
   1052         left = YAxis(self.ymin, self.ymax, self.xmin, self.yticks, self.yminiticks, self.ylabels, self.ylogbase,
   1053                      None, None, None, self.text_attr, **self.axis_attr)
   1054         right = YAxis(self.ymin, self.ymax, self.xmax, self.yticks, self.yminiticks, self.y2labels, self.ylogbase,
   1055                       None, None, None, self.text_attr, **self.axis_attr)
   1056         bottom = XAxis(self.xmin, self.xmax, self.ymin, self.xticks, self.xminiticks, self.xlabels, self.xlogbase,
   1057                        None, None, None, self.text_attr, **self.axis_attr)
   1058         top = XAxis(self.xmin, self.xmax, self.ymax, self.xticks, self.xminiticks, self.x2labels, self.xlogbase,
   1059                     None, None, None, self.text_attr, **self.axis_attr)
   1060 
   1061         left.tick_start = -self.tick_length
   1062         left.tick_end = 0
   1063         left.minitick_start = -self.minitick_length
   1064         left.minitick_end = 0.
   1065         left.text_start = self.text_yaxis_offset
   1066 
   1067         right.tick_start = 0.
   1068         right.tick_end = self.tick_length
   1069         right.minitick_start = 0.
   1070         right.minitick_end = self.minitick_length
   1071         right.text_start = -self.text_yaxis_offset
   1072         right.text_attr["text-anchor"] = "start"
   1073 
   1074         bottom.tick_start = 0.
   1075         bottom.tick_end = self.tick_length
   1076         bottom.minitick_start = 0.
   1077         bottom.minitick_end = self.minitick_length
   1078         bottom.text_start = -self.text_xaxis_offset
   1079 
   1080         top.tick_start = -self.tick_length
   1081         top.tick_end = 0.
   1082         top.minitick_start = -self.minitick_length
   1083         top.minitick_end = 0.
   1084         top.text_start = self.text_xaxis_offset
   1085         top.text_attr["dominant-baseline"] = "text-after-edge"
   1086 
   1087         output = Fig(*self.d).SVG(self.last_window)
   1088         output.prepend(left.SVG(self.last_window))
   1089         output.prepend(bottom.SVG(self.last_window))
   1090         output.prepend(right.SVG(self.last_window))
   1091         output.prepend(top.SVG(self.last_window))
   1092 
   1093         if self.xtitle is not None:
   1094             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))
   1095         if self.ytitle is not None:
   1096             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))
   1097         return output
   1098 
   1099 ######################################################################
   1100 
   1101 def pathtoPath(svg):
   1102     """Converts SVG("path", d="...") into Path(d=[...])."""
   1103     if not isinstance(svg, SVG) or svg.t != "path":
   1104         raise TypeError, "Only SVG <path /> objects can be converted into Paths"
   1105     attr = dict(svg.attr)
   1106     d = attr["d"]
   1107     del attr["d"]
   1108     for key in attr.keys():
   1109         if not isinstance(key, str):
   1110             value = attr[key]
   1111             del attr[key]
   1112             attr[str(key)] = value
   1113     return Path(d, **attr)
   1114 
   1115 
   1116 class Path:
   1117     """Path represents an SVG path, an arbitrary set of curves and
   1118     straight segments. Unlike SVG("path", d="..."), Path stores
   1119     coordinates as a list of numbers, rather than a string, so that it is
   1120     transformable in a Fig.
   1121 
   1122     Path(d, attribute=value)
   1123 
   1124     d                       required        path data
   1125     attribute=value pairs   keyword list    SVG attributes
   1126 
   1127     See http://www.w3.org/TR/SVG/paths.html for specification of paths
   1128     from text.
   1129 
   1130     Internally, Path data is a list of tuples with these definitions:
   1131 
   1132         * ("Z/z",): close the current path
   1133         * ("H/h", x) or ("V/v", y): a horizontal or vertical line
   1134           segment to x or y
   1135         * ("M/m/L/l/T/t", x, y, global): moveto, lineto, or smooth
   1136           quadratic curveto point (x, y). If global=True, (x, y) should
   1137           not be transformed.
   1138         * ("S/sQ/q", cx, cy, cglobal, x, y, global): polybezier or
   1139           smooth quadratic curveto point (x, y) using (cx, cy) as a
   1140           control point. If cglobal or global=True, (cx, cy) or (x, y)
   1141           should not be transformed.
   1142         * ("C/c", c1x, c1y, c1global, c2x, c2y, c2global, x, y, global):
   1143           cubic curveto point (x, y) using (c1x, c1y) and (c2x, c2y) as
   1144           control points. If c1global, c2global, or global=True, (c1x, c1y),
   1145           (c2x, c2y), or (x, y) should not be transformed.
   1146         * ("A/a", rx, ry, rglobal, x-axis-rotation, angle, large-arc-flag,
   1147           sweep-flag, x, y, global): arcto point (x, y) using the
   1148           aforementioned parameters.
   1149         * (",/.", rx, ry, rglobal, angle, x, y, global): an ellipse at
   1150           point (x, y) with radii (rx, ry). If angle is 0, the whole
   1151           ellipse is drawn; otherwise, a partial ellipse is drawn.
   1152     """
   1153     defaults = {}
   1154 
   1155     def __repr__(self):
   1156         return "<Path (%d nodes) %s>" % (len(self.d), self.attr)
   1157 
   1158     def __init__(self, d=[], **attr):
   1159         if isinstance(d, basestring):
   1160             self.d = self.parse(d)
   1161         else:
   1162             self.d = list(d)
   1163 
   1164         self.attr = dict(self.defaults)
   1165         self.attr.update(attr)
   1166 
   1167     def parse_whitespace(self, index, pathdata):
   1168         """Part of Path's text-command parsing algorithm; used internally."""
   1169         while index < len(pathdata) and pathdata[index] in (" ", "\t", "\r", "\n", ","):
   1170             index += 1
   1171         return index, pathdata
   1172 
   1173     def parse_command(self, index, pathdata):
   1174         """Part of Path's text-command parsing algorithm; used internally."""
   1175         index, pathdata = self.parse_whitespace(index, pathdata)
   1176 
   1177         if index >= len(pathdata):
   1178             return None, index, pathdata
   1179         command = pathdata[index]
   1180         if "A" <= command <= "Z" or "a" <= command <= "z":
   1181             index += 1
   1182             return command, index, pathdata
   1183         else:
   1184             return None, index, pathdata
   1185 
   1186     def parse_number(self, index, pathdata):
   1187         """Part of Path's text-command parsing algorithm; used internally."""
   1188         index, pathdata = self.parse_whitespace(index, pathdata)
   1189 
   1190         if index >= len(pathdata):
   1191             return None, index, pathdata
   1192         first_digit = pathdata[index]
   1193 
   1194         if "0" <= first_digit <= "9" or first_digit in ("-", "+", "."):
   1195             start = index
   1196             while index < len(pathdata) and ("0" <= pathdata[index] <= "9" or pathdata[index] in ("-", "+", ".", "e", "E")):
   1197                 index += 1
   1198             end = index
   1199 
   1200             index = end
   1201             return float(pathdata[start:end]), index, pathdata
   1202         else:
   1203             return None, index, pathdata
   1204 
   1205     def parse_boolean(self, index, pathdata):
   1206         """Part of Path's text-command parsing algorithm; used internally."""
   1207         index, pathdata = self.parse_whitespace(index, pathdata)
   1208 
   1209         if index >= len(pathdata):
   1210             return None, index, pathdata
   1211         first_digit = pathdata[index]
   1212 
   1213         if first_digit in ("0", "1"):
   1214             index += 1
   1215             return int(first_digit), index, pathdata
   1216         else:
   1217             return None, index, pathdata
   1218 
   1219     def parse(self, pathdata):
   1220         """Parses text-commands, converting them into a list of tuples.
   1221         Called by the constructor."""
   1222         output = []
   1223         index = 0
   1224         while True:
   1225             command, index, pathdata = self.parse_command(index, pathdata)
   1226             index, pathdata = self.parse_whitespace(index, pathdata)
   1227 
   1228             if command is None and index == len(pathdata):
   1229                 break  # this is the normal way out of the loop
   1230             if command in ("Z", "z"):
   1231                 output.append((command,))
   1232 
   1233             ######################
   1234             elif command in ("H", "h", "V", "v"):
   1235                 errstring = "Path command \"%s\" requires a number at index %d" % (command, index)
   1236                 num1, index, pathdata = self.parse_number(index, pathdata)
   1237                 if num1 is None:
   1238                     raise ValueError, errstring
   1239 
   1240                 while num1 is not None:
   1241                     output.append((command, num1))
   1242                     num1, index, pathdata = self.parse_number(index, pathdata)
   1243 
   1244             ######################
   1245             elif command in ("M", "m", "L", "l", "T", "t"):
   1246                 errstring = "Path command \"%s\" requires an x,y pair at index %d" % (command, index)
   1247                 num1, index, pathdata = self.parse_number(index, pathdata)
   1248                 num2, index, pathdata = self.parse_number(index, pathdata)
   1249 
   1250                 if num1 is None:
   1251                     raise ValueError, errstring
   1252 
   1253                 while num1 is not None:
   1254                     if num2 is None:
   1255                         raise ValueError, errstring
   1256                     output.append((command, num1, num2, False))
   1257 
   1258                     num1, index, pathdata = self.parse_number(index, pathdata)
   1259                     num2, index, pathdata = self.parse_number(index, pathdata)
   1260 
   1261             ######################
   1262             elif command in ("S", "s", "Q", "q"):
   1263                 errstring = "Path command \"%s\" requires a cx,cy,x,y quadruplet at index %d" % (command, index)
   1264                 num1, index, pathdata = self.parse_number(index, pathdata)
   1265                 num2, index, pathdata = self.parse_number(index, pathdata)
   1266                 num3, index, pathdata = self.parse_number(index, pathdata)
   1267                 num4, index, pathdata = self.parse_number(index, pathdata)
   1268 
   1269                 if num1 is None:
   1270                     raise ValueError, errstring
   1271 
   1272                 while num1 is not None:
   1273                     if num2 is None or num3 is None or num4 is None:
   1274                         raise ValueError, errstring
   1275                     output.append((command, num1, num2, False, num3, num4, False))
   1276 
   1277                     num1, index, pathdata = self.parse_number(index, pathdata)
   1278                     num2, index, pathdata = self.parse_number(index, pathdata)
   1279                     num3, index, pathdata = self.parse_number(index, pathdata)
   1280                     num4, index, pathdata = self.parse_number(index, pathdata)
   1281 
   1282             ######################
   1283             elif command in ("C", "c"):
   1284                 errstring = "Path command \"%s\" requires a c1x,c1y,c2x,c2y,x,y sextuplet at index %d" % (command, index)
   1285                 num1, index, pathdata = self.parse_number(index, pathdata)
   1286                 num2, index, pathdata = self.parse_number(index, pathdata)
   1287                 num3, index, pathdata = self.parse_number(index, pathdata)
   1288                 num4, index, pathdata = self.parse_number(index, pathdata)
   1289                 num5, index, pathdata = self.parse_number(index, pathdata)
   1290                 num6, index, pathdata = self.parse_number(index, pathdata)
   1291 
   1292                 if num1 is None:
   1293                     raise ValueError, errstring
   1294 
   1295                 while num1 is not None:
   1296                     if num2 is None or num3 is None or num4 is None or num5 is None or num6 is None:
   1297                         raise ValueError, errstring
   1298 
   1299                     output.append((command, num1, num2, False, num3, num4, False, num5, num6, False))
   1300 
   1301                     num1, index, pathdata = self.parse_number(index, pathdata)
   1302                     num2, index, pathdata = self.parse_number(index, pathdata)
   1303                     num3, index, pathdata = self.parse_number(index, pathdata)
   1304                     num4, index, pathdata = self.parse_number(index, pathdata)
   1305                     num5, index, pathdata = self.parse_number(index, pathdata)
   1306                     num6, index, pathdata = self.parse_number(index, pathdata)
   1307 
   1308             ######################
   1309             elif command in ("A", "a"):
   1310                 errstring = "Path command \"%s\" requires a rx,ry,angle,large-arc-flag,sweep-flag,x,y septuplet at index %d" % (command, index)
   1311                 num1, index, pathdata = self.parse_number(index, pathdata)
   1312                 num2, index, pathdata = self.parse_number(index, pathdata)
   1313                 num3, index, pathdata = self.parse_number(index, pathdata)
   1314                 num4, index, pathdata = self.parse_boolean(index, pathdata)
   1315                 num5, index, pathdata = self.parse_boolean(index, pathdata)
   1316                 num6, index, pathdata = self.parse_number(index, pathdata)
   1317                 num7, index, pathdata = self.parse_number(index, pathdata)
   1318 
   1319                 if num1 is None:
   1320                     raise ValueError, errstring
   1321 
   1322                 while num1 is not None:
   1323                     if num2 is None or num3 is None or num4 is None or num5 is None or num6 is None or num7 is None:
   1324                         raise ValueError, errstring
   1325 
   1326                     output.append((command, num1, num2, False, num3, num4, num5, num6, num7, False))
   1327 
   1328                     num1, index, pathdata = self.parse_number(index, pathdata)
   1329                     num2, index, pathdata = self.parse_number(index, pathdata)
   1330                     num3, index, pathdata = self.parse_number(index, pathdata)
   1331                     num4, index, pathdata = self.parse_boolean(index, pathdata)
   1332                     num5, index, pathdata = self.parse_boolean(index, pathdata)
   1333                     num6, index, pathdata = self.parse_number(index, pathdata)
   1334                     num7, index, pathdata = self.parse_number(index, pathdata)
   1335 
   1336         return output
   1337 
   1338     def SVG(self, trans=None):
   1339         """Apply the transformation "trans" and return an SVG object."""
   1340         if isinstance(trans, basestring):
   1341             trans = totrans(trans)
   1342 
   1343         x, y, X, Y = None, None, None, None
   1344         output = []
   1345         for datum in self.d:
   1346             if not isinstance(datum, (tuple, list)):
   1347                 raise TypeError, "pathdata elements must be tuples/lists"
   1348 
   1349             command = datum[0]
   1350 
   1351             ######################
   1352             if command in ("Z", "z"):
   1353                 x, y, X, Y = None, None, None, None
   1354                 output.append("Z")
   1355 
   1356             ######################
   1357             elif command in ("H", "h", "V", "v"):
   1358                 command, num1 = datum
   1359 
   1360                 if command == "H" or (command == "h" and x is None):
   1361                     x = num1
   1362                 elif command == "h":
   1363                     x += num1
   1364                 elif command == "V" or (command == "v" and y is None):
   1365                     y = num1
   1366                 elif command == "v":
   1367                     y += num1
   1368 
   1369                 if trans is None:
   1370                     X, Y = x, y
   1371                 else:
   1372                     X, Y = trans(x, y)
   1373 
   1374                 output.append("L%g %g" % (X, Y))
   1375 
   1376             ######################
   1377             elif command in ("M", "m", "L", "l", "T", "t"):
   1378                 command, num1, num2, isglobal12 = datum
   1379 
   1380                 if trans is None or isglobal12:
   1381                     if command.isupper() or X is None or Y is None:
   1382                         X, Y = num1, num2
   1383                     else:
   1384                         X += num1
   1385                         Y += num2
   1386                     x, y = X, Y
   1387 
   1388                 else:
   1389                     if command.isupper() or x is None or y is None:
   1390                         x, y = num1, num2
   1391                     else:
   1392                         x += num1
   1393                         y += num2
   1394                     X, Y = trans(x, y)
   1395 
   1396                 COMMAND = command.capitalize()
   1397                 output.append("%s%g %g" % (COMMAND, X, Y))
   1398 
   1399             ######################
   1400             elif command in ("S", "s", "Q", "q"):
   1401                 command, num1, num2, isglobal12, num3, num4, isglobal34 = datum
   1402 
   1403                 if trans is None or isglobal12:
   1404                     if command.isupper() or X is None or Y is None:
   1405                         CX, CY = num1, num2
   1406                     else:
   1407                         CX = X + num1
   1408                         CY = Y + num2
   1409 
   1410                 else:
   1411                     if command.isupper() or x is None or y is None:
   1412                         cx, cy = num1, num2
   1413                     else:
   1414                         cx = x + num1
   1415                         cy = y + num2
   1416                     CX, CY = trans(cx, cy)
   1417 
   1418                 if trans is None or isglobal34:
   1419                     if command.isupper() or X is None or Y is None:
   1420                         X, Y = num3, num4
   1421                     else:
   1422                         X += num3
   1423                         Y += num4
   1424                     x, y = X, Y
   1425 
   1426                 else:
   1427                     if command.isupper() or x is None or y is None:
   1428                         x, y = num3, num4
   1429                     else:
   1430                         x += num3
   1431                         y += num4
   1432                     X, Y = trans(x, y)
   1433 
   1434                 COMMAND = command.capitalize()
   1435                 output.append("%s%g %g %g %g" % (COMMAND, CX, CY, X, Y))
   1436 
   1437             ######################
   1438             elif command in ("C", "c"):
   1439                 command, num1, num2, isglobal12, num3, num4, isglobal34, num5, num6, isglobal56 = datum
   1440 
   1441                 if trans is None or isglobal12:
   1442                     if command.isupper() or X is None or Y is None:
   1443                         C1X, C1Y = num1, num2
   1444                     else:
   1445                         C1X = X + num1
   1446                         C1Y = Y + num2
   1447 
   1448                 else:
   1449                     if command.isupper() or x is None or y is None:
   1450                         c1x, c1y = num1, num2
   1451                     else:
   1452                         c1x = x + num1
   1453                         c1y = y + num2
   1454                     C1X, C1Y = trans(c1x, c1y)
   1455 
   1456                 if trans is None or isglobal34:
   1457                     if command.isupper() or X is None or Y is None:
   1458                         C2X, C2Y = num3, num4
   1459                     else:
   1460                         C2X = X + num3
   1461                         C2Y = Y + num4
   1462 
   1463                 else:
   1464                     if command.isupper() or x is None or y is None:
   1465                         c2x, c2y = num3, num4
   1466                     else:
   1467                         c2x = x + num3
   1468                         c2y = y + num4
   1469                     C2X, C2Y = trans(c2x, c2y)
   1470 
   1471                 if trans is None or isglobal56:
   1472                     if command.isupper() or X is None or Y is None:
   1473                         X, Y = num5, num6
   1474                     else:
   1475                         X += num5
   1476                         Y += num6
   1477                     x, y = X, Y
   1478 
   1479                 else:
   1480                     if command.isupper() or x is None or y is None:
   1481                         x, y = num5, num6
   1482                     else:
   1483                         x += num5
   1484                         y += num6
   1485                     X, Y = trans(x, y)
   1486 
   1487                 COMMAND = command.capitalize()
   1488                 output.append("%s%g %g %g %g %g %g" % (COMMAND, C1X, C1Y, C2X, C2Y, X, Y))
   1489 
   1490             ######################
   1491             elif command in ("A", "a"):
   1492                 command, num1, num2, isglobal12, angle, large_arc_flag, sweep_flag, num3, num4, isglobal34 = datum
   1493 
   1494                 oldx, oldy = x, y
   1495                 OLDX, OLDY = X, Y
   1496 
   1497                 if trans is None or isglobal34:
   1498                     if command.isupper() or X is None or Y is None:
   1499                         X, Y = num3, num4
   1500                     else:
   1501                         X += num3
   1502                         Y += num4
   1503                     x, y = X, Y
   1504 
   1505                 else:
   1506                     if command.isupper() or x is None or y is None:
   1507                         x, y = num3, num4
   1508                     else:
   1509                         x += num3
   1510                         y += num4
   1511                     X, Y = trans(x, y)
   1512 
   1513                 if x is not None and y is not None:
   1514                     centerx, centery = (x + oldx)/2., (y + oldy)/2.
   1515                 CENTERX, CENTERY = (X + OLDX)/2., (Y + OLDY)/2.
   1516 
   1517                 if trans is None or isglobal12:
   1518                     RX = CENTERX + num1
   1519                     RY = CENTERY + num2
   1520 
   1521                 else:
   1522                     rx = centerx + num1
   1523                     ry = centery + num2
   1524                     RX, RY = trans(rx, ry)
   1525 
   1526                 COMMAND = command.capitalize()
   1527                 output.append("%s%g %g %g %d %d %g %g" % (COMMAND, RX - CENTERX, RY - CENTERY, angle, large_arc_flag, sweep_flag, X, Y))
   1528 
   1529             elif command in (",", "."):
   1530                 command, num1, num2, isglobal12, angle, num3, num4, isglobal34 = datum
   1531                 if trans is None or isglobal34:
   1532                     if command == "." or X is None or Y is None:
   1533                         X, Y = num3, num4
   1534                     else:
   1535                         X += num3
   1536                         Y += num4
   1537                         x, y = None, None
   1538 
   1539                 else:
   1540                     if command == "." or x is None or y is None:
   1541                         x, y = num3, num4
   1542                     else:
   1543                         x += num3
   1544                         y += num4
   1545                     X, Y = trans(x, y)
   1546 
   1547                 if trans is None or isglobal12:
   1548                     RX = X + num1
   1549                     RY = Y + num2
   1550 
   1551                 else:
   1552                     rx = x + num1
   1553                     ry = y + num2
   1554                     RX, RY = trans(rx, ry)
   1555 
   1556                 RX, RY = RX - X, RY - Y
   1557 
   1558                 X1, Y1 = X + RX * math.cos(angle*math.pi/180.), Y + RX * math.sin(angle*math.pi/180.)
   1559                 X2, Y2 = X + RY * math.sin(angle*math.pi/180.), Y - RY * math.cos(angle*math.pi/180.)
   1560                 X3, Y3 = X - RX * math.cos(angle*math.pi/180.), Y - RX * math.sin(angle*math.pi/180.)
   1561                 X4, Y4 = X - RY * math.sin(angle*math.pi/180.), Y + RY * math.cos(angle*math.pi/180.)
   1562 
   1563                 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" % (
   1564                               X1, Y1, RX, RY, angle, X2, Y2, RX, RY, angle, X3, Y3, RX, RY, angle, X4, Y4, RX, RY, angle, X1, Y1))
   1565 
   1566         return SVG("path", d="".join(output), **self.attr)
   1567 
   1568 ######################################################################
   1569 
   1570 def funcRtoC(expr, var="t", globals=None, locals=None):
   1571     """Converts a complex "z(t)" string to a function acceptable for Curve.
   1572 
   1573     expr    required        string in the form "z(t)"
   1574     var     default="t"     name of the independent variable
   1575     globals default=None    dict of global variables used in the expression;
   1576                             you may want to use Python's builtin globals()
   1577     locals  default=None    dict of local variables
   1578     """
   1579     if locals is None:
   1580         locals = {}  # python 2.3's eval() won't accept None
   1581     g = cmath.__dict__
   1582     if globals is not None:
   1583         g.update(globals)
   1584     output = eval("lambda %s: (%s)" % (var, expr), g, locals)
   1585     split = lambda z: (z.real, z.imag)
   1586     output2 = lambda t: split(output(t))
   1587     set_func_name(output2, "%s -> %s" % (var, expr))
   1588     return output2
   1589 
   1590 
   1591 def funcRtoR2(expr, var="t", globals=None, locals=None):
   1592     """Converts a "f(t), g(t)" string to a function acceptable for Curve.
   1593 
   1594     expr    required        string in the form "f(t), g(t)"
   1595     var     default="t"     name of the independent variable
   1596     globals default=None    dict of global variables used in the expression;
   1597                             you may want to use Python's builtin globals()
   1598     locals  default=None    dict of local variables
   1599     """
   1600     if locals is None:
   1601         locals = {}  # python 2.3's eval() won't accept None
   1602     g = math.__dict__
   1603     if globals is not None:
   1604         g.update(globals)
   1605     output = eval("lambda %s: (%s)" % (var, expr), g, locals)
   1606     set_func_name(output, "%s -> %s" % (var, expr))
   1607     return output
   1608 
   1609 
   1610 def funcRtoR(expr, var="x", globals=None, locals=None):
   1611     """Converts a "f(x)" string to a function acceptable for Curve.
   1612 
   1613     expr    required        string in the form "f(x)"
   1614     var     default="x"     name of the independent variable
   1615     globals default=None    dict of global variables used in the expression;
   1616                             you may want to use Python's builtin globals()
   1617     locals  default=None    dict of local variables
   1618     """
   1619     if locals is None:
   1620         locals = {}  # python 2.3's eval() won't accept None
   1621     g = math.__dict__
   1622     if globals is not None:
   1623         g.update(globals)
   1624     output = eval("lambda %s: (%s, %s)" % (var, var, expr), g, locals)
   1625     set_func_name(output, "%s -> %s" % (var, expr))
   1626     return output
   1627 
   1628 
   1629 class Curve:
   1630     """Draws a parametric function as a path.
   1631 
   1632     Curve(f, low, high, loop, attribute=value)
   1633 
   1634     f                      required         a Python callable or string in
   1635                                             the form "f(t), g(t)"
   1636     low, high              required         left and right endpoints
   1637     loop                   default=False    if True, connect the endpoints
   1638     attribute=value pairs  keyword list     SVG attributes
   1639     """
   1640     defaults = {}
   1641     random_sampling = True
   1642     recursion_limit = 15
   1643     linearity_limit = 0.05
   1644     discontinuity_limit = 5.
   1645 
   1646     def __repr__(self):
   1647         return "<Curve %s [%s, %s] %s>" % (self.f, self.low, self.high, self.attr)
   1648 
   1649     def __init__(self, f, low, high, loop=False, **attr):
   1650         self.f = f
   1651         self.low = low
   1652         self.high = high
   1653         self.loop = loop
   1654 
   1655         self.attr = dict(self.defaults)
   1656         self.attr.update(attr)
   1657 
   1658     ### nested class Sample
   1659     class Sample:
   1660         def __repr__(self):
   1661             t, x, y, X, Y = self.t, self.x, self.y, self.X, self.Y
   1662             if t is not None:
   1663                 t = "%g" % t
   1664             if x is not None:
   1665                 x = "%g" % x
   1666             if y is not None:
   1667                 y = "%g" % y
   1668             if X is not None:
   1669                 X = "%g" % X
   1670             if Y is not None:
   1671                 Y = "%g" % Y
   1672             return "<Curve.Sample t=%s x=%s y=%s X=%s Y=%s>" % (t, x, y, X, Y)
   1673 
   1674         def __init__(self, t):
   1675             self.t = t
   1676 
   1677         def link(self, left, right):
   1678             self.left, self.right = left, right
   1679 
   1680         def evaluate(self, f, trans):
   1681             self.x, self.y = f(self.t)
   1682             if trans is None:
   1683                 self.X, self.Y = self.x, self.y
   1684             else:
   1685                 self.X, self.Y = trans(self.x, self.y)
   1686     ### end Sample
   1687 
   1688     ### nested class Samples
   1689     class Samples:
   1690         def __repr__(self):
   1691             return "<Curve.Samples (%d samples)>" % len(self)
   1692 
   1693         def __init__(self, left, right):
   1694             self.left, self.right = left, right
   1695 
   1696         def __len__(self):
   1697             count = 0
   1698             current = self.left
   1699             while current is not None:
   1700                 count += 1
   1701                 current = current.right
   1702             return count
   1703 
   1704         def __iter__(self):
   1705             self.current = self.left
   1706             return self
   1707 
   1708         def next(self):
   1709             current = self.current
   1710             if current is None:
   1711                 raise StopIteration
   1712             self.current = self.current.right
   1713             return current
   1714     ### end nested class
   1715 
   1716     def sample(self, trans=None):
   1717         """Adaptive-sampling algorithm that chooses the best sample points
   1718         for a parametric curve between two endpoints and detects
   1719         discontinuities.  Called by SVG()."""
   1720         oldrecursionlimit = sys.getrecursionlimit()
   1721         sys.setrecursionlimit(self.recursion_limit + 100)
   1722         try:
   1723             # the best way to keep all the information while sampling is to make a linked list
   1724             if not (self.low < self.high):
   1725                 raise ValueError, "low must be less than high"
   1726             low, high = self.Sample(float(self.low)), self.Sample(float(self.high))
   1727             low.link(None, high)
   1728             high.link(low, None)
   1729 
   1730             low.evaluate(self.f, trans)
   1731             high.evaluate(self.f, trans)
   1732 
   1733             # adaptive sampling between the low and high points
   1734             self.subsample(low, high, 0, trans)
   1735 
   1736             # Prune excess points where the curve is nearly linear
   1737             left = low
   1738             while left.right is not None:
   1739                 # increment mid and right
   1740                 mid = left.right
   1741                 right = mid.right
   1742                 if (right is not None and
   1743                     left.X is not None and left.Y is not None and
   1744                     mid.X is not None and mid.Y is not None and
   1745                     right.X is not None and right.Y is not None):
   1746                     numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
   1747                     denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
   1748                     if denom != 0. and abs(numer/denom) < self.linearity_limit:
   1749                         # drop mid (the garbage collector will get it)
   1750                         left.right = right
   1751                         right.left = left
   1752                     else:
   1753                         # increment left
   1754                         left = left.right
   1755                 else:
   1756                     left = left.right
   1757 
   1758             self.last_samples = self.Samples(low, high)
   1759 
   1760         finally:
   1761             sys.setrecursionlimit(oldrecursionlimit)
   1762 
   1763     def subsample(self, left, right, depth, trans=None):
   1764         """Part of the adaptive-sampling algorithm that chooses the best
   1765         sample points.  Called by sample()."""
   1766 
   1767         if self.random_sampling:
   1768             mid = self.Sample(left.t + random.uniform(0.3, 0.7) * (right.t - left.t))
   1769         else:
   1770             mid = self.Sample(left.t + 0.5 * (right.t - left.t))
   1771 
   1772         left.right = mid
   1773         right.left = mid
   1774         mid.link(left, right)
   1775         mid.evaluate(self.f, trans)
   1776 
   1777         # calculate the distance of closest approach of mid to the line between left and right
   1778         numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
   1779         denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
   1780 
   1781         # if we haven't sampled enough or left fails to be close enough to right, or mid fails to be linear enough...
   1782         if (depth < 3 or
   1783             (denom == 0 and left.t != right.t) or
   1784             denom > self.discontinuity_limit or
   1785             (denom != 0. and abs(numer/denom) > self.linearity_limit)):
   1786 
   1787             # and we haven't sampled too many points
   1788             if depth < self.recursion_limit:
   1789                 self.subsample(left, mid, depth+1, trans)
   1790                 self.subsample(mid, right, depth+1, trans)
   1791 
   1792             else:
   1793                 # We've sampled many points and yet it's still not a small linear gap.
   1794                 # Break the line: it's a discontinuity
   1795                 mid.y = mid.Y = None
   1796 
   1797     def SVG(self, trans=None):
   1798         """Apply the transformation "trans" and return an SVG object."""
   1799         return self.Path(trans).SVG()
   1800 
   1801     def Path(self, trans=None, local=False):
   1802         """Apply the transformation "trans" and return a Path object in
   1803         global coordinates.  If local=True, return a Path in local coordinates
   1804         (which must be transformed again)."""
   1805 
   1806         if isinstance(trans, basestring):
   1807             trans = totrans(trans)
   1808         if isinstance(self.f, basestring):
   1809             self.f = funcRtoR2(self.f)
   1810 
   1811         self.sample(trans)
   1812 
   1813         output = []
   1814         for s in self.last_samples:
   1815             if s.X is not None and s.Y is not None:
   1816                 if s.left is None or s.left.Y is None:
   1817                     command = "M"
   1818                 else:
   1819                     command = "L"
   1820 
   1821                 if local:
   1822                     output.append((command, s.x, s.y, False))
   1823                 else:
   1824                     output.append((command, s.X, s.Y, True))
   1825 
   1826         if self.loop:
   1827             output.append(("Z",))
   1828         return Path(output, **self.attr)
   1829 
   1830 ######################################################################
   1831 
   1832 class Poly:
   1833     """Draws a curve specified by a sequence of points. The curve may be
   1834     piecewise linear, like a polygon, or a Bezier curve.
   1835 
   1836     Poly(d, mode, loop, attribute=value)
   1837 
   1838     d                       required        list of tuples representing points
   1839                                             and possibly control points
   1840     mode                    default="L"     "lines", "bezier", "velocity",
   1841                                             "foreback", "smooth", or an abbreviation
   1842     loop                    default=False   if True, connect the first and last
   1843                                             point, closing the loop
   1844     attribute=value pairs   keyword list    SVG attributes
   1845 
   1846     The format of the tuples in d depends on the mode.
   1847 
   1848     "lines"/"L"         d=[(x,y), (x,y), ...]
   1849                                             piecewise-linear segments joining the (x,y) points
   1850     "bezier"/"B"        d=[(x, y, c1x, c1y, c2x, c2y), ...]
   1851                                             Bezier curve with two control points (control points
   1852                                             preceed (x,y), as in SVG paths). If (c1x,c1y) and
   1853                                             (c2x,c2y) both equal (x,y), you get a linear
   1854                                             interpolation ("lines")
   1855     "velocity"/"V"      d=[(x, y, vx, vy), ...]
   1856                                             curve that passes through (x,y) with velocity (vx,vy)
   1857                                             (one unit of arclength per unit time); in other words,
   1858                                             (vx,vy) is the tangent vector at (x,y). If (vx,vy) is
   1859                                             (0,0), you get a linear interpolation ("lines").
   1860     "foreback"/"F"      d=[(x, y, bx, by, fx, fy), ...]
   1861                                             like "velocity" except that there is a left derivative
   1862                                             (bx,by) and a right derivative (fx,fy). If (bx,by)
   1863                                             equals (fx,fy) (with no minus sign), you get a
   1864                                             "velocity" curve
   1865     "smooth"/"S"        d=[(x,y), (x,y), ...]
   1866                                             a "velocity" interpolation with (vx,vy)[i] equal to
   1867                                             ((x,y)[i+1] - (x,y)[i-1])/2: the minimal derivative
   1868     """
   1869     defaults = {}
   1870 
   1871     def __repr__(self):
   1872         return "<Poly (%d nodes) mode=%s loop=%s %s>" % (
   1873                len(self.d), self.mode, repr(self.loop), self.attr)
   1874 
   1875     def __init__(self, d=[], mode="L", loop=False, **attr):
   1876         self.d = list(d)
   1877         self.mode = mode
   1878         self.loop = loop
   1879 
   1880         self.attr = dict(self.defaults)
   1881         self.attr.update(attr)
   1882 
   1883     def SVG(self, trans=None):
   1884         """Apply the transformation "trans" and return an SVG object."""
   1885         return self.Path(trans).SVG()
   1886 
   1887     def Path(self, trans=None, local=False):
   1888         """Apply the transformation "trans" and return a Path object in
   1889         global coordinates.  If local=True, return a Path in local coordinates
   1890         (which must be transformed again)."""
   1891         if isinstance(trans, basestring):
   1892             trans = totrans(trans)
   1893 
   1894         if self.mode[0] == "L" or self.mode[0] == "l":
   1895             mode = "L"
   1896         elif self.mode[0] == "B" or self.mode[0] == "b":
   1897             mode = "B"
   1898         elif self.mode[0] == "V" or self.mode[0] == "v":
   1899             mode = "V"
   1900         elif self.mode[0] == "F" or self.mode[0] == "f":
   1901             mode = "F"
   1902         elif self.mode[0] == "S" or self.mode[0] == "s":
   1903             mode = "S"
   1904 
   1905             vx, vy = [0.]*len(self.d), [0.]*len(self.d)
   1906             for i in xrange(len(self.d)):
   1907                 inext = (i+1) % len(self.d)
   1908                 iprev = (i-1) % len(self.d)
   1909 
   1910                 vx[i] = (self.d[inext][0] - self.d[iprev][0])/2.
   1911                 vy[i] = (self.d[inext][1] - self.d[iprev][1])/2.
   1912                 if not self.loop and (i == 0 or i == len(self.d)-1):
   1913                     vx[i], vy[i] = 0., 0.
   1914 
   1915         else:
   1916             raise ValueError, "mode must be \"lines\", \"bezier\", \"velocity\", \"foreback\", \"smooth\", or an abbreviation"
   1917 
   1918         d = []
   1919         indexes = range(len(self.d))
   1920         if self.loop and len(self.d) > 0:
   1921             indexes.append(0)
   1922 
   1923         for i in indexes:
   1924             inext = (i+1) % len(self.d)
   1925             iprev = (i-1) % len(self.d)
   1926 
   1927             x, y = self.d[i][0], self.d[i][1]
   1928 
   1929             if trans is None:
   1930                 X, Y = x, y
   1931             else:
   1932                 X, Y = trans(x, y)
   1933 
   1934             if d == []:
   1935                 if local:
   1936                     d.append(("M", x, y, False))
   1937                 else:
   1938                     d.append(("M", X, Y, True))
   1939 
   1940             elif mode == "L":
   1941                 if local:
   1942                     d.append(("L", x, y, False))
   1943                 else:
   1944                     d.append(("L", X, Y, True))
   1945 
   1946             elif mode == "B":
   1947                 c1x, c1y = self.d[i][2], self.d[i][3]
   1948                 if trans is None:
   1949                     C1X, C1Y = c1x, c1y
   1950                 else:
   1951                     C1X, C1Y = trans(c1x, c1y)
   1952 
   1953                 c2x, c2y = self.d[i][4], self.d[i][5]
   1954                 if trans is None:
   1955                     C2X, C2Y = c2x, c2y
   1956                 else:
   1957                     C2X, C2Y = trans(c2x, c2y)
   1958 
   1959                 if local:
   1960                     d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
   1961                 else:
   1962                     d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
   1963 
   1964             elif mode == "V":
   1965                 c1x, c1y = self.d[iprev][2]/3. + self.d[iprev][0], self.d[iprev][3]/3. + self.d[iprev][1]
   1966                 c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
   1967 
   1968                 if trans is None:
   1969                     C1X, C1Y = c1x, c1y
   1970                 else:
   1971                     C1X, C1Y = trans(c1x, c1y)
   1972                 if trans is None:
   1973                     C2X, C2Y = c2x, c2y
   1974                 else:
   1975                     C2X, C2Y = trans(c2x, c2y)
   1976 
   1977                 if local:
   1978                     d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
   1979                 else:
   1980                     d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
   1981 
   1982             elif mode == "F":
   1983                 c1x, c1y = self.d[iprev][4]/3. + self.d[iprev][0], self.d[iprev][5]/3. + self.d[iprev][1]
   1984                 c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
   1985 
   1986                 if trans is None:
   1987                     C1X, C1Y = c1x, c1y
   1988                 else:
   1989                     C1X, C1Y = trans(c1x, c1y)
   1990                 if trans is None:
   1991                     C2X, C2Y = c2x, c2y
   1992                 else:
   1993                     C2X, C2Y = trans(c2x, c2y)
   1994 
   1995                 if local:
   1996                     d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
   1997                 else:
   1998                     d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
   1999 
   2000             elif mode == "S":
   2001                 c1x, c1y = vx[iprev]/3. + self.d[iprev][0], vy[iprev]/3. + self.d[iprev][1]
   2002                 c2x, c2y = vx[i]/-3. + x, vy[i]/-3. + y
   2003 
   2004                 if trans is None:
   2005                     C1X, C1Y = c1x, c1y
   2006                 else:
   2007                     C1X, C1Y = trans(c1x, c1y)
   2008                 if trans is None:
   2009                     C2X, C2Y = c2x, c2y
   2010                 else:
   2011                     C2X, C2Y = trans(c2x, c2y)
   2012 
   2013                 if local:
   2014                     d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
   2015                 else:
   2016                     d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
   2017 
   2018         if self.loop and len(self.d) > 0:
   2019             d.append(("Z",))
   2020 
   2021         return Path(d, **self.attr)
   2022 
   2023 ######################################################################
   2024 
   2025 class Text:
   2026     """Draws a text string at a specified point in local coordinates.
   2027 
   2028     x, y                   required      location of the point in local coordinates
   2029     d                      required      text/Unicode string
   2030     attribute=value pairs  keyword list  SVG attributes
   2031     """
   2032 
   2033     defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
   2034 
   2035     def __repr__(self):
   2036         return "<Text %s at (%g, %g) %s>" % (repr(self.d), self.x, self.y, self.attr)
   2037 
   2038     def __init__(self, x, y, d, **attr):
   2039         self.x = x
   2040         self.y = y
   2041         self.d = unicode(d)
   2042         self.attr = dict(self.defaults)
   2043         self.attr.update(attr)
   2044 
   2045     def SVG(self, trans=None):
   2046         """Apply the transformation "trans" and return an SVG object."""
   2047         if isinstance(trans, basestring):
   2048             trans = totrans(trans)
   2049 
   2050         X, Y = self.x, self.y
   2051         if trans is not None:
   2052             X, Y = trans(X, Y)
   2053         return SVG("text", self.d, x=X, y=Y, **self.attr)
   2054 
   2055 
   2056 class TextGlobal:
   2057     """Draws a text string at a specified point in global coordinates.
   2058 
   2059     x, y                   required      location of the point in global coordinates
   2060     d                      required      text/Unicode string
   2061     attribute=value pairs  keyword list  SVG attributes
   2062     """
   2063     defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
   2064 
   2065     def __repr__(self):
   2066         return "<TextGlobal %s at (%s, %s) %s>" % (repr(self.d), str(self.x), str(self.y), self.attr)
   2067 
   2068     def __init__(self, x, y, d, **attr):
   2069         self.x = x
   2070         self.y = y
   2071         self.d = unicode(d)
   2072         self.attr = dict(self.defaults)
   2073         self.attr.update(attr)
   2074 
   2075     def SVG(self, trans=None):
   2076         """Apply the transformation "trans" and return an SVG object."""
   2077         return SVG("text", self.d, x=self.x, y=self.y, **self.attr)
   2078 
   2079 ######################################################################
   2080 
   2081 _symbol_templates = {"dot": SVG("symbol", SVG("circle", cx=0, cy=0, r=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"),
   2082                     "box": SVG("symbol", SVG("rect", x1=-1, y1=-1, x2=1, y2=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"),
   2083                     "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"),
   2084                     "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"),
   2085                     }
   2086 
   2087 def make_symbol(id, shape="dot", **attr):
   2088     """Creates a new instance of an SVG symbol to avoid cross-linking objects.
   2089 
   2090     id                    required         a new identifier (string/Unicode)
   2091     shape                 default="dot"  the shape name from _symbol_templates
   2092     attribute=value list  keyword list     modify the SVG attributes of the new symbol
   2093     """
   2094     output = copy.deepcopy(_symbol_templates[shape])
   2095     for i in output.sub:
   2096         i.attr.update(attr_preprocess(attr))
   2097     output["id"] = id
   2098     return output
   2099 
   2100 _circular_dot = make_symbol("circular_dot")
   2101 
   2102 
   2103 class Dots:
   2104     """Dots draws SVG symbols at a set of points.
   2105 
   2106     d                      required               list of (x,y) points
   2107     symbol                 default=None           SVG symbol or a new identifier to
   2108                                                   label an auto-generated symbol;
   2109                                                   if None, use pre-defined _circular_dot
   2110     width, height          default=1, 1           width and height of the symbols
   2111                                                   in SVG coordinates
   2112     attribute=value pairs  keyword list           SVG attributes
   2113     """
   2114     defaults = {}
   2115 
   2116     def __repr__(self):
   2117         return "<Dots (%d nodes) %s>" % (len(self.d), self.attr)
   2118 
   2119     def __init__(self, d=[], symbol=None, width=1., height=1., **attr):
   2120         self.d = list(d)
   2121         self.width = width
   2122         self.height = height
   2123 
   2124         self.attr = dict(self.defaults)
   2125         self.attr.update(attr)
   2126 
   2127         if symbol is None:
   2128             self.symbol = _circular_dot
   2129         elif isinstance(symbol, SVG):
   2130             self.symbol = symbol
   2131         else:
   2132             self.symbol = make_symbol(symbol)
   2133 
   2134     def SVG(self, trans=None):
   2135         """Apply the transformation "trans" and return an SVG object."""
   2136         if isinstance(trans, basestring):
   2137             trans = totrans(trans)
   2138 
   2139         output = SVG("g", SVG("defs", self.symbol))
   2140         id = "#%s" % self.symbol["id"]
   2141 
   2142         for p in self.d:
   2143             x, y = p[0], p[1]
   2144 
   2145             if trans is None:
   2146                 X, Y = x, y
   2147             else:
   2148                 X, Y = trans(x, y)
   2149 
   2150             item = SVG("use", x=X, y=Y, xlink__href=id)
   2151             if self.width is not None:
   2152                 item["width"] = self.width
   2153             if self.height is not None:
   2154                 item["height"] = self.height
   2155             output.append(item)
   2156 
   2157         return output
   2158 
   2159 ######################################################################
   2160 
   2161 _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"),
   2162                     "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"),
   2163                     }
   2164 
   2165 def make_marker(id, shape, **attr):
   2166     """Creates a new instance of an SVG marker to avoid cross-linking objects.
   2167 
   2168     id                     required         a new identifier (string/Unicode)
   2169     shape                  required         the shape name from _marker_templates
   2170     attribute=value list   keyword list     modify the SVG attributes of the new marker
   2171     """
   2172     output = copy.deepcopy(_marker_templates[shape])
   2173     for i in output.sub:
   2174         i.attr.update(attr_preprocess(attr))
   2175     output["id"] = id
   2176     return output
   2177 
   2178 
   2179 class Line(Curve):
   2180     """Draws a line between two points.
   2181 
   2182     Line(x1, y1, x2, y2, arrow_start, arrow_end, attribute=value)
   2183 
   2184     x1, y1                  required        the starting point
   2185     x2, y2                  required        the ending point
   2186     arrow_start             default=None    if an identifier string/Unicode,
   2187                                             draw a new arrow object at the
   2188                                             beginning of the line; if a marker,
   2189                                             draw that marker instead
   2190     arrow_end               default=None    same for the end of the line
   2191     attribute=value pairs   keyword list    SVG attributes
   2192     """
   2193     defaults = {}
   2194 
   2195     def __repr__(self):
   2196         return "<Line (%g, %g) to (%g, %g) %s>" % (
   2197                self.x1, self.y1, self.x2, self.y2, self.attr)
   2198 
   2199     def __init__(self, x1, y1, x2, y2, arrow_start=None, arrow_end=None, **attr):
   2200         self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
   2201         self.arrow_start, self.arrow_end = arrow_start, arrow_end
   2202 
   2203         self.attr = dict(self.defaults)
   2204         self.attr.update(attr)
   2205 
   2206     def SVG(self, trans=None):
   2207         """Apply the transformation "trans" and return an SVG object."""
   2208 
   2209         line = self.Path(trans).SVG()
   2210 
   2211         if ((self.arrow_start != False and self.arrow_start is not None) or
   2212             (self.arrow_end != False and self.arrow_end is not None)):
   2213             defs = SVG("defs")
   2214 
   2215             if self.arrow_start != False and self.arrow_start is not None:
   2216                 if isinstance(self.arrow_start, SVG):
   2217                     defs.append(self.arrow_start)
   2218                     line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
   2219                 elif isinstance(self.arrow_start, basestring):
   2220                     defs.append(make_marker(self.arrow_start, "arrow_start"))
   2221                     line.attr["marker-start"] = "url(#%s)" % self.arrow_start
   2222                 else:
   2223                     raise TypeError, "arrow_start must be False/None or an id string for the new marker"
   2224 
   2225             if self.arrow_end != False and self.arrow_end is not None:
   2226                 if isinstance(self.arrow_end, SVG):
   2227                     defs.append(self.arrow_end)
   2228                     line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
   2229                 elif isinstance(self.arrow_end, basestring):
   2230                     defs.append(make_marker(self.arrow_end, "arrow_end"))
   2231                     line.attr["marker-end"] = "url(#%s)" % self.arrow_end
   2232                 else:
   2233                     raise TypeError, "arrow_end must be False/None or an id string for the new marker"
   2234 
   2235             return SVG("g", defs, line)
   2236 
   2237         return line
   2238 
   2239     def Path(self, trans=None, local=False):
   2240         """Apply the transformation "trans" and return a Path object in
   2241         global coordinates.  If local=True, return a Path in local coordinates
   2242         (which must be transformed again)."""
   2243         self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1 + t*(self.y2 - self.y1))
   2244         self.low = 0.
   2245         self.high = 1.
   2246         self.loop = False
   2247 
   2248         if trans is None:
   2249             return Path([("M", self.x1, self.y1, not local), ("L", self.x2, self.y2, not local)], **self.attr)
   2250         else:
   2251             return Curve.Path(self, trans, local)
   2252 
   2253 
   2254 class LineGlobal:
   2255     """Draws a line between two points, one or both of which is in
   2256     global coordinates.
   2257 
   2258     Line(x1, y1, x2, y2, lcoal1, local2, arrow_start, arrow_end, attribute=value)
   2259 
   2260     x1, y1                  required        the starting point
   2261     x2, y2                  required        the ending point
   2262     local1                  default=False   if True, interpret first point as a
   2263                                             local coordinate (apply transform)
   2264     local2                  default=False   if True, interpret second point as a
   2265                                             local coordinate (apply transform)
   2266     arrow_start             default=None    if an identifier string/Unicode,
   2267                                             draw a new arrow object at the
   2268                                             beginning of the line; if a marker,
   2269                                             draw that marker instead
   2270     arrow_end               default=None    same for the end of the line
   2271     attribute=value pairs   keyword list    SVG attributes
   2272     """
   2273     defaults = {}
   2274 
   2275     def __repr__(self):
   2276         local1, local2 = "", ""
   2277         if self.local1:
   2278             local1 = "L"
   2279         if self.local2:
   2280             local2 = "L"
   2281 
   2282         return "<LineGlobal %s(%s, %s) to %s(%s, %s) %s>" % (
   2283                local1, str(self.x1), str(self.y1), local2, str(self.x2), str(self.y2), self.attr)
   2284 
   2285     def __init__(self, x1, y1, x2, y2, local1=False, local2=False, arrow_start=None, arrow_end=None, **attr):
   2286         self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
   2287         self.local1, self.local2 = local1, local2
   2288         self.arrow_start, self.arrow_end = arrow_start, arrow_end
   2289 
   2290         self.attr = dict(self.defaults)
   2291         self.attr.update(attr)
   2292 
   2293     def SVG(self, trans=None):
   2294         """Apply the transformation "trans" and return an SVG object."""
   2295         if isinstance(trans, basestring):
   2296             trans = totrans(trans)
   2297 
   2298         X1, Y1, X2, Y2 = self.x1, self.y1, self.x2, self.y2
   2299 
   2300         if self.local1:
   2301             X1, Y1 = trans(X1, Y1)
   2302         if self.local2:
   2303             X2, Y2 = trans(X2, Y2)
   2304 
   2305         line = SVG("path", d="M%s %s L%s %s" % (X1, Y1, X2, Y2), **self.attr)
   2306 
   2307         if ((self.arrow_start != False and self.arrow_start is not None) or
   2308             (self.arrow_end != False and self.arrow_end is not None)):
   2309             defs = SVG("defs")
   2310 
   2311             if self.arrow_start != False and self.arrow_start is not None:
   2312                 if isinstance(self.arrow_start, SVG):
   2313                     defs.append(self.arrow_start)
   2314                     line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
   2315                 elif isinstance(self.arrow_start, basestring):
   2316                     defs.append(make_marker(self.arrow_start, "arrow_start"))
   2317                     line.attr["marker-start"] = "url(#%s)" % self.arrow_start
   2318                 else:
   2319                     raise TypeError, "arrow_start must be False/None or an id string for the new marker"
   2320 
   2321             if self.arrow_end != False and self.arrow_end is not None:
   2322                 if isinstance(self.arrow_end, SVG):
   2323                     defs.append(self.arrow_end)
   2324                     line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
   2325                 elif isinstance(self.arrow_end, basestring):
   2326                     defs.append(make_marker(self.arrow_end, "arrow_end"))
   2327                     line.attr["marker-end"] = "url(#%s)" % self.arrow_end
   2328                 else:
   2329                     raise TypeError, "arrow_end must be False/None or an id string for the new marker"
   2330 
   2331             return SVG("g", defs, line)
   2332 
   2333         return line
   2334 
   2335 
   2336 class VLine(Line):
   2337     """Draws a vertical line.
   2338 
   2339     VLine(y1, y2, x, attribute=value)
   2340 
   2341     y1, y2                  required        y range
   2342     x                       required        x position
   2343     attribute=value pairs   keyword list    SVG attributes
   2344     """
   2345     defaults = {}
   2346 
   2347     def __repr__(self):
   2348         return "<VLine (%g, %g) at x=%s %s>" % (self.y1, self.y2, self.x, self.attr)
   2349 
   2350     def __init__(self, y1, y2, x, **attr):
   2351         self.x = x
   2352         self.attr = dict(self.defaults)
   2353         self.attr.update(attr)
   2354         Line.__init__(self, x, y1, x, y2, **self.attr)
   2355 
   2356     def Path(self, trans=None, local=False):
   2357         """Apply the transformation "trans" and return a Path object in
   2358         global coordinates.  If local=True, return a Path in local coordinates
   2359         (which must be transformed again)."""
   2360         self.x1 = self.x
   2361         self.x2 = self.x
   2362         return Line.Path(self, trans, local)
   2363 
   2364 
   2365 class HLine(Line):
   2366     """Draws a horizontal line.
   2367 
   2368     HLine(x1, x2, y, attribute=value)
   2369 
   2370     x1, x2                  required        x range
   2371     y                       required        y position
   2372     attribute=value pairs   keyword list    SVG attributes
   2373     """
   2374     defaults = {}
   2375 
   2376     def __repr__(self):
   2377         return "<HLine (%g, %g) at y=%s %s>" % (self.x1, self.x2, self.y, self.attr)
   2378 
   2379     def __init__(self, x1, x2, y, **attr):
   2380         self.y = y
   2381         self.attr = dict(self.defaults)
   2382         self.attr.update(attr)
   2383         Line.__init__(self, x1, y, x2, y, **self.attr)
   2384 
   2385     def Path(self, trans=None, local=False):
   2386         """Apply the transformation "trans" and return a Path object in
   2387         global coordinates.  If local=True, return a Path in local coordinates
   2388         (which must be transformed again)."""
   2389         self.y1 = self.y
   2390         self.y2 = self.y
   2391         return Line.Path(self, trans, local)
   2392 
   2393 ######################################################################
   2394 
   2395 class Rect(Curve):
   2396     """Draws a rectangle.
   2397 
   2398     Rect(x1, y1, x2, y2, attribute=value)
   2399 
   2400     x1, y1                  required        the starting point
   2401     x2, y2                  required        the ending point
   2402     attribute=value pairs   keyword list    SVG attributes
   2403     """
   2404     defaults = {}
   2405 
   2406     def __repr__(self):
   2407         return "<Rect (%g, %g), (%g, %g) %s>" % (
   2408                self.x1, self.y1, self.x2, self.y2, self.attr)
   2409 
   2410     def __init__(self, x1, y1, x2, y2, **attr):
   2411         self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
   2412 
   2413         self.attr = dict(self.defaults)
   2414         self.attr.update(attr)
   2415 
   2416     def SVG(self, trans=None):
   2417         """Apply the transformation "trans" and return an SVG object."""
   2418         return self.Path(trans).SVG()
   2419 
   2420     def Path(self, trans=None, local=False):
   2421         """Apply the transformation "trans" and return a Path object in
   2422         global coordinates.  If local=True, return a Path in local coordinates
   2423         (which must be transformed again)."""
   2424         if trans is None:
   2425             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)
   2426 
   2427         else:
   2428             self.low = 0.
   2429             self.high = 1.
   2430             self.loop = False
   2431 
   2432             self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1)
   2433             d1 = Curve.Path(self, trans, local).d
   2434 
   2435             self.f = lambda t: (self.x2, self.y1 + t*(self.y2 - self.y1))
   2436             d2 = Curve.Path(self, trans, local).d
   2437             del d2[0]
   2438 
   2439             self.f = lambda t: (self.x2 + t*(self.x1 - self.x2), self.y2)
   2440             d3 = Curve.Path(self, trans, local).d
   2441             del d3[0]
   2442 
   2443             self.f = lambda t: (self.x1, self.y2 + t*(self.y1 - self.y2))
   2444             d4 = Curve.Path(self, trans, local).d
   2445             del d4[0]
   2446 
   2447             return Path(d=(d1 + d2 + d3 + d4 + [("Z",)]), **self.attr)
   2448 
   2449 ######################################################################
   2450 
   2451 class Ellipse(Curve):
   2452     """Draws an ellipse from a semimajor vector (ax,ay) and a semiminor
   2453     length (b).
   2454 
   2455     Ellipse(x, y, ax, ay, b, attribute=value)
   2456 
   2457     x, y                    required        the center of the ellipse/circle
   2458     ax, ay                  required        a vector indicating the length
   2459                                             and direction of the semimajor axis
   2460     b                       required        the length of the semiminor axis.
   2461                                             If equal to sqrt(ax2 + ay2), the
   2462                                             ellipse is a circle
   2463     attribute=value pairs   keyword list    SVG attributes
   2464 
   2465     (If sqrt(ax**2 + ay**2) is less than b, then (ax,ay) is actually the
   2466     semiminor axis.)
   2467     """
   2468     defaults = {}
   2469 
   2470     def __repr__(self):
   2471         return "<Ellipse (%g, %g) a=(%g, %g), b=%g %s>" % (
   2472                self.x, self.y, self.ax, self.ay, self.b, self.attr)
   2473 
   2474     def __init__(self, x, y, ax, ay, b, **attr):
   2475         self.x, self.y, self.ax, self.ay, self.b = x, y, ax, ay, b
   2476 
   2477         self.attr = dict(self.defaults)
   2478         self.attr.update(attr)
   2479 
   2480     def SVG(self, trans=None):
   2481         """Apply the transformation "trans" and return an SVG object."""
   2482         return self.Path(trans).SVG()
   2483 
   2484     def Path(self, trans=None, local=False):
   2485         """Apply the transformation "trans" and return a Path object in
   2486         global coordinates.  If local=True, return a Path in local coordinates
   2487         (which must be transformed again)."""
   2488         angle = math.atan2(self.ay, self.ax) + math.pi/2.
   2489         bx = self.b * math.cos(angle)
   2490         by = self.b * math.sin(angle)
   2491 
   2492         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))
   2493         self.low = -math.pi
   2494         self.high = math.pi
   2495         self.loop = True
   2496         return Curve.Path(self, trans, local)
   2497 
   2498 ######################################################################
   2499 
   2500 def unumber(x):
   2501     """Converts numbers to a Unicode string, taking advantage of special
   2502     Unicode characters to make nice minus signs and scientific notation.
   2503     """
   2504     output = u"%g" % x
   2505 
   2506     if output[0] == u"-":
   2507         output = u"\u2013" + output[1:]
   2508 
   2509     index = output.find(u"e")
   2510     if index != -1:
   2511         uniout = unicode(output[:index]) + u"\u00d710"
   2512         saw_nonzero = False
   2513         for n in output[index+1:]:
   2514             if n == u"+":
   2515                 pass # uniout += u"\u207a"
   2516             elif n == u"-":
   2517                 uniout += u"\u207b"
   2518             elif n == u"0":
   2519                 if saw_nonzero:
   2520                     uniout += u"\u2070"
   2521             elif n == u"1":
   2522                 saw_nonzero = True
   2523                 uniout += u"\u00b9"
   2524             elif n == u"2":
   2525                 saw_nonzero = True
   2526                 uniout += u"\u00b2"
   2527             elif n == u"3":
   2528                 saw_nonzero = True
   2529                 uniout += u"\u00b3"
   2530             elif u"4" <= n <= u"9":
   2531                 saw_nonzero = True
   2532                 if saw_nonzero:
   2533                     uniout += eval("u\"\\u%x\"" % (0x2070 + ord(n) - ord(u"0")))
   2534             else:
   2535                 uniout += n
   2536 
   2537         if uniout[:2] == u"1\u00d7":
   2538             uniout = uniout[2:]
   2539         return uniout
   2540 
   2541     return output
   2542 
   2543 
   2544 class Ticks:
   2545     """Superclass for all graphics primitives that draw ticks,
   2546     miniticks, and tick labels.  This class only draws the ticks.
   2547 
   2548     Ticks(f, low, high, ticks, miniticks, labels, logbase, arrow_start,
   2549           arrow_end, text_attr, attribute=value)
   2550 
   2551     f                       required        parametric function along which ticks
   2552                                             will be drawn; has the same format as
   2553                                             the function used in Curve
   2554     low, high               required        range of the independent variable
   2555     ticks                   default=-10     request ticks according to the standard
   2556                                             tick specification (see below)
   2557     miniticks               default=True    request miniticks according to the
   2558                                             standard minitick specification (below)
   2559     labels                  True            request tick labels according to the
   2560                                             standard tick label specification (below)
   2561     logbase                 default=None    if a number, the axis is logarithmic with
   2562                                             ticks at the given base (usually 10)
   2563     arrow_start             default=None    if a new string identifier, draw an arrow
   2564                                             at the low-end of the axis, referenced by
   2565                                             that identifier; if an SVG marker object,
   2566                                             use that marker
   2567     arrow_end               default=None    if a new string identifier, draw an arrow
   2568                                             at the high-end of the axis, referenced by
   2569                                             that identifier; if an SVG marker object,
   2570                                             use that marker
   2571     text_attr               default={}      SVG attributes for the text labels
   2572     attribute=value pairs   keyword list    SVG attributes for the tick marks
   2573 
   2574     Standard tick specification:
   2575 
   2576         * True: same as -10 (below).
   2577         * Positive number N: draw exactly N ticks, including the endpoints. To
   2578           subdivide an axis into 10 equal-sized segments, ask for 11 ticks.
   2579         * Negative number -N: draw at least N ticks. Ticks will be chosen with
   2580           "natural" values, multiples of 2 or 5.
   2581         * List of values: draw a tick mark at each value.
   2582         * Dict of value, label pairs: draw a tick mark at each value, labeling
   2583           it with the given string. This lets you say things like {3.14159: "pi"}.
   2584         * False or None: no ticks.
   2585 
   2586     Standard minitick specification:
   2587 
   2588         * True: draw miniticks with "natural" values, more closely spaced than
   2589           the ticks.
   2590         * Positive number N: draw exactly N miniticks, including the endpoints.
   2591           To subdivide an axis into 100 equal-sized segments, ask for 101 miniticks.
   2592         * Negative number -N: draw at least N miniticks.
   2593         * List of values: draw a minitick mark at each value.
   2594         * False or None: no miniticks.
   2595 
   2596     Standard tick label specification:
   2597 
   2598         * True: use the unumber function (described below)
   2599         * Format string: standard format strings, e.g. "%5.2f" for 12.34
   2600         * Python callable: function that converts numbers to strings
   2601         * False or None: no labels
   2602     """
   2603     defaults = {"stroke-width": "0.25pt", }
   2604     text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
   2605     tick_start = -1.5
   2606     tick_end = 1.5
   2607     minitick_start = -0.75
   2608     minitick_end = 0.75
   2609     text_start = 2.5
   2610     text_angle = 0.
   2611 
   2612     def __repr__(self):
   2613         return "<Ticks %s from %s to %s ticks=%s labels=%s %s>" % (
   2614                self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr)
   2615 
   2616     def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None,
   2617                  arrow_start=None, arrow_end=None, text_attr={}, **attr):
   2618         self.f = f
   2619         self.low = low
   2620         self.high = high
   2621         self.ticks = ticks
   2622         self.miniticks = miniticks
   2623         self.labels = labels
   2624         self.logbase = logbase
   2625         self.arrow_start = arrow_start
   2626         self.arrow_end = arrow_end
   2627 
   2628         self.attr = dict(self.defaults)
   2629         self.attr.update(attr)
   2630 
   2631         self.text_attr = dict(self.text_defaults)
   2632         self.text_attr.update(text_attr)
   2633 
   2634     def orient_tickmark(self, t, trans=None):
   2635         """Return the position, normalized local x vector, normalized
   2636         local y vector, and angle of a tick at position t.
   2637 
   2638         Normally only used internally.
   2639         """
   2640         if isinstance(trans, basestring):
   2641             trans = totrans(trans)
   2642         if trans is None:
   2643             f = self.f
   2644         else:
   2645             f = lambda t: trans(*self.f(t))
   2646 
   2647         eps = _epsilon * abs(self.high - self.low)
   2648 
   2649         X, Y = f(t)
   2650         Xprime, Yprime = f(t + eps)
   2651         xhatx, xhaty = (Xprime - X)/eps, (Yprime - Y)/eps
   2652 
   2653         norm = math.sqrt(xhatx**2 + xhaty**2)
   2654         if norm != 0:
   2655             xhatx, xhaty = xhatx/norm, xhaty/norm
   2656         else:
   2657             xhatx, xhaty = 1., 0.
   2658 
   2659         angle = math.atan2(xhaty, xhatx) + math.pi/2.
   2660         yhatx, yhaty = math.cos(angle), math.sin(angle)
   2661 
   2662         return (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle
   2663 
   2664     def SVG(self, trans=None):
   2665         """Apply the transformation "trans" and return an SVG object."""
   2666         if isinstance(trans, basestring):
   2667             trans = totrans(trans)
   2668 
   2669         self.last_ticks, self.last_miniticks = self.interpret()
   2670         tickmarks = Path([], **self.attr)
   2671         minitickmarks = Path([], **self.attr)
   2672         output = SVG("g")
   2673 
   2674         if ((self.arrow_start != False and self.arrow_start is not None) or
   2675             (self.arrow_end != False and self.arrow_end is not None)):
   2676             defs = SVG("defs")
   2677 
   2678             if self.arrow_start != False and self.arrow_start is not None:
   2679                 if isinstance(self.arrow_start, SVG):
   2680                     defs.append(self.arrow_start)
   2681                 elif isinstance(self.arrow_start, basestring):
   2682                     defs.append(make_marker(self.arrow_start, "arrow_start"))
   2683                 else:
   2684                     raise TypeError, "arrow_start must be False/None or an id string for the new marker"
   2685 
   2686             if self.arrow_end != False and self.arrow_end is not None:
   2687                 if isinstance(self.arrow_end, SVG):
   2688                     defs.append(self.arrow_end)
   2689                 elif isinstance(self.arrow_end, basestring):
   2690                     defs.append(make_marker(self.arrow_end, "arrow_end"))
   2691                 else:
   2692                     raise TypeError, "arrow_end must be False/None or an id string for the new marker"
   2693 
   2694             output.append(defs)
   2695 
   2696         eps = _epsilon * (self.high - self.low)
   2697 
   2698         for t, label in self.last_ticks.items():
   2699             (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
   2700 
   2701             if ((not self.arrow_start or abs(t - self.low) > eps) and
   2702                 (not self.arrow_end or abs(t - self.high) > eps)):
   2703                 tickmarks.d.append(("M", X - yhatx*self.tick_start, Y - yhaty*self.tick_start, True))
   2704                 tickmarks.d.append(("L", X - yhatx*self.tick_end, Y - yhaty*self.tick_end, True))
   2705 
   2706             angle = (angle - math.pi/2.)*180./math.pi + self.text_angle
   2707 
   2708             ########### a HACK! ############ (to be removed when Inkscape handles baselines)
   2709             if _hacks["inkscape-text-vertical-shift"]:
   2710                 if self.text_start > 0:
   2711                     X += math.cos(angle*math.pi/180. + math.pi/2.) * 2.
   2712                     Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2.
   2713                 else:
   2714                     X += math.cos(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
   2715                     Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
   2716             ########### end hack ###########
   2717 
   2718             if label != "":
   2719                 output.append(SVG("text", label, transform="translate(%g, %g) rotate(%g)" %
   2720                                   (X - yhatx*self.text_start, Y - yhaty*self.text_start, angle), **self.text_attr))
   2721 
   2722         for t in self.last_miniticks:
   2723             skip = False
   2724             for tt in self.last_ticks.keys():
   2725                 if abs(t - tt) < eps:
   2726                     skip = True
   2727                     break
   2728             if not skip:
   2729                 (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
   2730 
   2731             if ((not self.arrow_start or abs(t - self.low) > eps) and
   2732                 (not self.arrow_end or abs(t - self.high) > eps)):
   2733                 minitickmarks.d.append(("M", X - yhatx*self.minitick_start, Y - yhaty*self.minitick_start, True))
   2734                 minitickmarks.d.append(("L", X - yhatx*self.minitick_end, Y - yhaty*self.minitick_end, True))
   2735 
   2736         output.prepend(tickmarks.SVG(trans))
   2737         output.prepend(minitickmarks.SVG(trans))
   2738         return output
   2739 
   2740     def interpret(self):
   2741         """Evaluate and return optimal ticks and miniticks according to
   2742         the standard minitick specification.
   2743 
   2744         Normally only used internally.
   2745         """
   2746 
   2747         if self.labels is None or self.labels == False:
   2748             format = lambda x: ""
   2749 
   2750         elif self.labels == True:
   2751             format = unumber
   2752 
   2753         elif isinstance(self.labels, basestring):
   2754             format = lambda x: (self.labels % x)
   2755 
   2756         elif callable(self.labels):
   2757             format = self.labels
   2758 
   2759         else:
   2760             raise TypeError, "labels must be None/False, True, a format string, or a number->string function"
   2761 
   2762         # Now for the ticks
   2763         ticks = self.ticks
   2764 
   2765         # Case 1: ticks is None/False
   2766         if ticks is None or ticks == False:
   2767             return {}, []
   2768 
   2769         # Case 2: ticks is the number of desired ticks
   2770         elif isinstance(ticks, (int, long)):
   2771             if ticks == True:
   2772                 ticks = -10
   2773 
   2774             if self.logbase is None:
   2775                 ticks = self.compute_ticks(ticks, format)
   2776             else:
   2777                 ticks = self.compute_logticks(self.logbase, ticks, format)
   2778 
   2779             # Now for the miniticks
   2780             if self.miniticks == True:
   2781                 if self.logbase is None:
   2782                     return ticks, self.compute_miniticks(ticks)
   2783                 else:
   2784                     return ticks, self.compute_logminiticks(self.logbase)
   2785 
   2786             elif isinstance(self.miniticks, (int, long)):
   2787                 return ticks, self.regular_miniticks(self.miniticks)
   2788 
   2789             elif getattr(self.miniticks, "__iter__", False):
   2790                 return ticks, self.miniticks
   2791 
   2792             elif self.miniticks == False or self.miniticks is None:
   2793                 return ticks, []
   2794 
   2795             else:
   2796                 raise TypeError, "miniticks must be None/False, True, a number of desired miniticks, or a list of numbers"
   2797 
   2798         # Cases 3 & 4: ticks is iterable
   2799         elif getattr(ticks, "__iter__", False):
   2800 
   2801             # Case 3: ticks is some kind of list
   2802             if not isinstance(ticks, dict):
   2803                 output = {}
   2804                 eps = _epsilon * (self.high - self.low)
   2805                 for x in ticks:
   2806                     if format == unumber and abs(x) < eps:
   2807                         output[x] = u"0"
   2808                     else:
   2809                         output[x] = format(x)
   2810                 ticks = output
   2811 
   2812             # Case 4: ticks is a dict
   2813             else:
   2814                 pass
   2815 
   2816             # Now for the miniticks
   2817             if self.miniticks == True:
   2818                 if self.logbase is None:
   2819                     return ticks, self.compute_miniticks(ticks)
   2820                 else:
   2821                     return ticks, self.compute_logminiticks(self.logbase)
   2822 
   2823             elif isinstance(self.miniticks, (int, long)):
   2824                 return ticks, self.regular_miniticks(self.miniticks)
   2825 
   2826             elif getattr(self.miniticks, "__iter__", False):
   2827                 return ticks, self.miniticks
   2828 
   2829             elif self.miniticks == False or self.miniticks is None:
   2830                 return ticks, []
   2831 
   2832             else:
   2833                 raise TypeError, "miniticks must be None/False, True, a number of desired miniticks, or a list of numbers"
   2834 
   2835         else:
   2836             raise TypeError, "ticks must be None/False, a number of desired ticks, a list of numbers, or a dictionary of explicit markers"
   2837 
   2838     def compute_ticks(self, N, format):
   2839         """Return less than -N or exactly N optimal linear ticks.
   2840 
   2841         Normally only used internally.
   2842         """
   2843         if self.low >= self.high:
   2844             raise ValueError, "low must be less than high"
   2845         if N == 1:
   2846             raise ValueError, "N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum"
   2847 
   2848         eps = _epsilon * (self.high - self.low)
   2849 
   2850         if N >= 0:
   2851             output = {}
   2852             x = self.low
   2853             for i in xrange(N):
   2854                 if format == unumber and abs(x) < eps:
   2855                     label = u"0"
   2856                 else:
   2857                     label = format(x)
   2858                 output[x] = label
   2859                 x += (self.high - self.low)/(N-1.)
   2860             return output
   2861 
   2862         N = -N
   2863 
   2864         counter = 0
   2865         granularity = 10**math.ceil(math.log10(max(abs(self.low), abs(self.high))))
   2866         lowN = math.ceil(1.*self.low / granularity)
   2867         highN = math.floor(1.*self.high / granularity)
   2868 
   2869         while lowN > highN:
   2870             countermod3 = counter % 3
   2871             if countermod3 == 0:
   2872                 granularity *= 0.5
   2873             elif countermod3 == 1:
   2874                 granularity *= 0.4
   2875             else:
   2876                 granularity *= 0.5
   2877             counter += 1
   2878             lowN = math.ceil(1.*self.low / granularity)
   2879             highN = math.floor(1.*self.high / granularity)
   2880 
   2881         last_granularity = granularity
   2882         last_trial = None
   2883 
   2884         while True:
   2885             trial = {}
   2886             for n in range(int(lowN), int(highN)+1):
   2887                 x = n * granularity
   2888                 if format == unumber and abs(x) < eps:
   2889                     label = u"0"
   2890                 else:
   2891                     label = format(x)
   2892                 trial[x] = label
   2893 
   2894             if int(highN)+1 - int(lowN) >= N:
   2895                 if last_trial is None:
   2896                     v1, v2 = self.low, self.high
   2897                     return {v1: format(v1), v2: format(v2)}
   2898                 else:
   2899                     low_in_ticks, high_in_ticks = False, False
   2900                     for t in last_trial.keys():
   2901                         if 1.*abs(t - self.low)/last_granularity < _epsilon:
   2902                             low_in_ticks = True
   2903                         if 1.*abs(t - self.high)/last_granularity < _epsilon:
   2904                             high_in_ticks = True
   2905 
   2906                     lowN = 1.*self.low / last_granularity
   2907                     highN = 1.*self.high / last_granularity
   2908                     if abs(lowN - round(lowN)) < _epsilon and not low_in_ticks:
   2909                         last_trial[self.low] = format(self.low)
   2910                     if abs(highN - round(highN)) < _epsilon and not high_in_ticks:
   2911                         last_trial[self.high] = format(self.high)
   2912                     return last_trial
   2913 
   2914             last_granularity = granularity
   2915             last_trial = trial
   2916 
   2917             countermod3 = counter % 3
   2918             if countermod3 == 0:
   2919                 granularity *= 0.5
   2920             elif countermod3 == 1:
   2921                 granularity *= 0.4
   2922             else:
   2923                 granularity *= 0.5
   2924             counter += 1
   2925             lowN = math.ceil(1.*self.low / granularity)
   2926             highN = math.floor(1.*self.high / granularity)
   2927 
   2928     def regular_miniticks(self, N):
   2929         """Return exactly N linear ticks.
   2930 
   2931         Normally only used internally.
   2932         """
   2933         output = []
   2934         x = self.low
   2935         for i in xrange(N):
   2936             output.append(x)
   2937             x += (self.high - self.low)/(N-1.)
   2938         return output
   2939 
   2940     def compute_miniticks(self, original_ticks):
   2941         """Return optimal linear miniticks, given a set of ticks.
   2942 
   2943         Normally only used internally.
   2944         """
   2945         if len(original_ticks) < 2:
   2946             original_ticks = ticks(self.low, self.high) # XXX ticks is undefined!
   2947         original_ticks = original_ticks.keys()
   2948         original_ticks.sort()
   2949 
   2950         if self.low > original_ticks[0] + _epsilon or self.high < original_ticks[-1] - _epsilon:
   2951             raise ValueError, "original_ticks {%g...%g} extend beyond [%g, %g]" % (original_ticks[0], original_ticks[-1], self.low, self.high)
   2952 
   2953         granularities = []
   2954         for i in range(len(original_ticks)-1):
   2955             granularities.append(original_ticks[i+1] - original_ticks[i])
   2956         spacing = 10**(math.ceil(math.log10(min(granularities)) - 1))
   2957 
   2958         output = []
   2959         x = original_ticks[0] - math.ceil(1.*(original_ticks[0] - self.low) / spacing) * spacing
   2960 
   2961         while x <= self.high:
   2962             if x >= self.low:
   2963                 already_in_ticks = False
   2964                 for t in original_ticks:
   2965                     if abs(x-t) < _epsilon * (self.high - self.low):
   2966                         already_in_ticks = True
   2967                 if not already_in_ticks:
   2968                     output.append(x)
   2969             x += spacing
   2970         return output
   2971 
   2972     def compute_logticks(self, base, N, format):
   2973         """Return less than -N or exactly N optimal logarithmic ticks.
   2974 
   2975         Normally only used internally.
   2976         """
   2977         if self.low >= self.high:
   2978             raise ValueError, "low must be less than high"
   2979         if N == 1:
   2980             raise ValueError, "N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum"
   2981 
   2982         eps = _epsilon * (self.high - self.low)
   2983 
   2984         if N >= 0:
   2985             output = {}
   2986             x = self.low
   2987             for i in xrange(N):
   2988                 if format == unumber and abs(x) < eps:
   2989                     label = u"0"
   2990                 else:
   2991                     label = format(x)
   2992                 output[x] = label
   2993                 x += (self.high - self.low)/(N-1.)
   2994             return output
   2995 
   2996         N = -N
   2997 
   2998         lowN = math.floor(math.log(self.low, base))
   2999         highN = math.ceil(math.log(self.high, base))
   3000         output = {}
   3001         for n in range(int(lowN), int(highN)+1):
   3002             x = base**n
   3003             label = format(x)
   3004             if self.low <= x <= self.high:
   3005                 output[x] = label
   3006 
   3007         for i in range(1, len(output)):
   3008             keys = output.keys()
   3009             keys.sort()
   3010             keys = keys[::i]
   3011             values = map(lambda k: output[k], keys)
   3012             if len(values) <= N:
   3013                 for k in output.keys():
   3014                     if k not in keys:
   3015                         output[k] = ""
   3016                 break
   3017 
   3018         if len(output) <= 2:
   3019             output2 = self.compute_ticks(N=-int(math.ceil(N/2.)), format=format)
   3020             lowest = min(output2)
   3021 
   3022             for k in output:
   3023                 if k < lowest:
   3024                     output2[k] = output[k]
   3025             output = output2
   3026 
   3027         return output
   3028 
   3029     def compute_logminiticks(self, base):
   3030         """Return optimal logarithmic miniticks, given a set of ticks.
   3031 
   3032         Normally only used internally.
   3033         """
   3034         if self.low >= self.high:
   3035             raise ValueError, "low must be less than high"
   3036 
   3037         lowN = math.floor(math.log(self.low, base))
   3038         highN = math.ceil(math.log(self.high, base))
   3039         output = []
   3040         num_ticks = 0
   3041         for n in range(int(lowN), int(highN)+1):
   3042             x = base**n
   3043             if self.low <= x <= self.high:
   3044                 num_ticks += 1
   3045             for m in range(2, int(math.ceil(base))):
   3046                 minix = m * x
   3047                 if self.low <= minix <= self.high:
   3048                     output.append(minix)
   3049 
   3050         if num_ticks <= 2:
   3051             return []
   3052         else:
   3053             return output
   3054 
   3055 ######################################################################
   3056 
   3057 class CurveAxis(Curve, Ticks):
   3058     """Draw an axis with tick marks along a parametric curve.
   3059 
   3060     CurveAxis(f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
   3061     text_attr, attribute=value)
   3062 
   3063     f                      required         a Python callable or string in
   3064                                             the form "f(t), g(t)", just like Curve
   3065     low, high              required         left and right endpoints
   3066     ticks                  default=-10      request ticks according to the standard
   3067                                             tick specification (see help(Ticks))
   3068     miniticks              default=True     request miniticks according to the
   3069                                             standard minitick specification
   3070     labels                 True             request tick labels according to the
   3071                                             standard tick label specification
   3072     logbase                default=None     if a number, the x axis is logarithmic
   3073                                             with ticks at the given base (10 being
   3074                                             the most common)
   3075     arrow_start            default=None     if a new string identifier, draw an
   3076                                             arrow at the low-end of the axis,
   3077                                             referenced by that identifier; if an
   3078                                             SVG marker object, use that marker
   3079     arrow_end              default=None     if a new string identifier, draw an
   3080                                             arrow at the high-end of the axis,
   3081                                             referenced by that identifier; if an
   3082                                             SVG marker object, use that marker
   3083     text_attr              default={}       SVG attributes for the text labels
   3084     attribute=value pairs  keyword list     SVG attributes
   3085     """
   3086     defaults = {"stroke-width": "0.25pt", }
   3087     text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
   3088 
   3089     def __repr__(self):
   3090         return "<CurveAxis %s [%s, %s] ticks=%s labels=%s %s>" % (
   3091                self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr)
   3092 
   3093     def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None,
   3094                  arrow_start=None, arrow_end=None, text_attr={}, **attr):
   3095         tattr = dict(self.text_defaults)
   3096         tattr.update(text_attr)
   3097         Curve.__init__(self, f, low, high)
   3098         Ticks.__init__(self, f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
   3099 
   3100     def SVG(self, trans=None):
   3101         """Apply the transformation "trans" and return an SVG object."""
   3102         func = Curve.SVG(self, trans)
   3103         ticks = Ticks.SVG(self, trans) # returns a <g />
   3104 
   3105         if self.arrow_start != False and self.arrow_start is not None:
   3106             if isinstance(self.arrow_start, basestring):
   3107                 func.attr["marker-start"] = "url(#%s)" % self.arrow_start
   3108             else:
   3109                 func.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
   3110 
   3111         if self.arrow_end != False and self.arrow_end is not None:
   3112             if isinstance(self.arrow_end, basestring):
   3113                 func.attr["marker-end"] = "url(#%s)" % self.arrow_end
   3114             else:
   3115                 func.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
   3116 
   3117         ticks.append(func)
   3118         return ticks
   3119 
   3120 
   3121 class LineAxis(Line, Ticks):
   3122     """Draws an axis with tick marks along a line.
   3123 
   3124     LineAxis(x1, y1, x2, y2, start, end, ticks, miniticks, labels, logbase,
   3125     arrow_start, arrow_end, text_attr, attribute=value)
   3126 
   3127     x1, y1                  required        starting point
   3128     x2, y2                  required        ending point
   3129     start, end              default=0, 1    values to start and end labeling
   3130     ticks                   default=-10     request ticks according to the standard
   3131                                             tick specification (see help(Ticks))
   3132     miniticks               default=True    request miniticks according to the
   3133                                             standard minitick specification
   3134     labels                  True            request tick labels according to the
   3135                                             standard tick label specification
   3136     logbase                 default=None    if a number, the x axis is logarithmic
   3137                                             with ticks at the given base (usually 10)
   3138     arrow_start             default=None    if a new string identifier, draw an arrow
   3139                                             at the low-end of the axis, referenced by
   3140                                             that identifier; if an SVG marker object,
   3141                                             use that marker
   3142     arrow_end               default=None    if a new string identifier, draw an arrow
   3143                                             at the high-end of the axis, referenced by
   3144                                             that identifier; if an SVG marker object,
   3145                                             use that marker
   3146     text_attr               default={}      SVG attributes for the text labels
   3147     attribute=value pairs   keyword list    SVG attributes
   3148     """
   3149     defaults = {"stroke-width": "0.25pt", }
   3150     text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
   3151 
   3152     def __repr__(self):
   3153         return "<LineAxis (%g, %g) to (%g, %g) ticks=%s labels=%s %s>" % (
   3154                self.x1, self.y1, self.x2, self.y2, str(self.ticks), str(self.labels), self.attr)
   3155 
   3156     def __init__(self, x1, y1, x2, y2, start=0., end=1., ticks=-10, miniticks=True, labels=True,
   3157                  logbase=None, arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
   3158         self.start = start
   3159         self.end = end
   3160         self.exclude = exclude
   3161         tattr = dict(self.text_defaults)
   3162         tattr.update(text_attr)
   3163         Line.__init__(self, x1, y1, x2, y2, **attr)
   3164         Ticks.__init__(self, None, None, None, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
   3165 
   3166     def interpret(self):
   3167         if self.exclude is not None and not (isinstance(self.exclude, (tuple, list)) and len(self.exclude) == 2 and
   3168                                              isinstance(self.exclude[0], (int, long, float)) and isinstance(self.exclude[1], (int, long, float))):
   3169             raise TypeError, "exclude must either be None or (low, high)"
   3170 
   3171         ticks, miniticks = Ticks.interpret(self)
   3172         if self.exclude is None:
   3173             return ticks, miniticks
   3174 
   3175         ticks2 = {}
   3176         for loc, label in ticks.items():
   3177             if self.exclude[0] <= loc <= self.exclude[1]:
   3178                 ticks2[loc] = ""
   3179             else:
   3180                 ticks2[loc] = label
   3181 
   3182         return ticks2, miniticks
   3183 
   3184     def SVG(self, trans=None):
   3185         """Apply the transformation "trans" and return an SVG object."""
   3186         line = Line.SVG(self, trans) # must be evaluated first, to set self.f, self.low, self.high
   3187 
   3188         f01 = self.f
   3189         self.f = lambda t: f01(1. * (t - self.start) / (self.end - self.start))
   3190         self.low = self.start
   3191         self.high = self.end
   3192 
   3193         if self.arrow_start != False and self.arrow_start is not None:
   3194             if isinstance(self.arrow_start, basestring):
   3195                 line.attr["marker-start"] = "url(#%s)" % self.arrow_start
   3196             else:
   3197                 line.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
   3198 
   3199         if self.arrow_end != False and self.arrow_end is not None:
   3200             if isinstance(self.arrow_end, basestring):
   3201                 line.attr["marker-end"] = "url(#%s)" % self.arrow_end
   3202             else:
   3203                 line.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
   3204 
   3205         ticks = Ticks.SVG(self, trans) # returns a <g />
   3206         ticks.append(line)
   3207         return ticks
   3208 
   3209 
   3210 class XAxis(LineAxis):
   3211     """Draws an x axis with tick marks.
   3212 
   3213     XAxis(xmin, xmax, aty, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
   3214     exclude, text_attr, attribute=value)
   3215 
   3216     xmin, xmax              required        the x range
   3217     aty                     default=0       y position to draw the axis
   3218     ticks                   default=-10     request ticks according to the standard
   3219                                             tick specification (see help(Ticks))
   3220     miniticks               default=True    request miniticks according to the
   3221                                             standard minitick specification
   3222     labels                  True            request tick labels according to the
   3223                                             standard tick label specification
   3224     logbase                 default=None    if a number, the x axis is logarithmic
   3225                                             with ticks at the given base (usually 10)
   3226     arrow_start             default=None    if a new string identifier, draw an arrow
   3227                                             at the low-end of the axis, referenced by
   3228                                             that identifier; if an SVG marker object,
   3229                                             use that marker
   3230     arrow_end               default=None    if a new string identifier, draw an arrow
   3231                                             at the high-end of the axis, referenced by
   3232                                             that identifier; if an SVG marker object,
   3233                                             use that marker
   3234     exclude                 default=None    if a (low, high) pair, don't draw text
   3235                                             labels within this range
   3236     text_attr               default={}      SVG attributes for the text labels
   3237     attribute=value pairs   keyword list    SVG attributes for all lines
   3238 
   3239     The exclude option is provided for Axes to keep text from overlapping
   3240     where the axes cross. Normal users are not likely to need it.
   3241     """
   3242     defaults = {"stroke-width": "0.25pt", }
   3243     text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, "dominant-baseline": "text-before-edge", }
   3244     text_start = -1.
   3245     text_angle = 0.
   3246 
   3247     def __repr__(self):
   3248         return "<XAxis (%g, %g) at y=%g ticks=%s labels=%s %s>" % (
   3249                self.xmin, self.xmax, self.aty, str(self.ticks), str(self.labels), self.attr) # XXX self.xmin/xmax undefd!
   3250 
   3251     def __init__(self, xmin, xmax, aty=0, ticks=-10, miniticks=True, labels=True, logbase=None,
   3252                  arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
   3253         self.aty = aty
   3254         tattr = dict(self.text_defaults)
   3255         tattr.update(text_attr)
   3256         LineAxis.__init__(self, xmin, aty, xmax, aty, xmin, xmax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
   3257 
   3258     def SVG(self, trans=None):
   3259         """Apply the transformation "trans" and return an SVG object."""
   3260         self.y1 = self.aty
   3261         self.y2 = self.aty
   3262         return LineAxis.SVG(self, trans)
   3263 
   3264 
   3265 class YAxis(LineAxis):
   3266     """Draws a y axis with tick marks.
   3267 
   3268     YAxis(ymin, ymax, atx, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
   3269     exclude, text_attr, attribute=value)
   3270 
   3271     ymin, ymax              required        the y range
   3272     atx                     default=0       x position to draw the axis
   3273     ticks                   default=-10     request ticks according to the standard
   3274                                             tick specification (see help(Ticks))
   3275     miniticks               default=True    request miniticks according to the
   3276                                             standard minitick specification
   3277     labels                  True            request tick labels according to the
   3278                                             standard tick label specification
   3279     logbase                 default=None    if a number, the y axis is logarithmic
   3280                                             with ticks at the given base (usually 10)
   3281     arrow_start             default=None    if a new string identifier, draw an arrow
   3282                                             at the low-end of the axis, referenced by
   3283                                             that identifier; if an SVG marker object,
   3284                                             use that marker
   3285     arrow_end               default=None    if a new string identifier, draw an arrow
   3286                                             at the high-end of the axis, referenced by
   3287                                             that identifier; if an SVG marker object,
   3288                                             use that marker
   3289     exclude                 default=None    if a (low, high) pair, don't draw text
   3290                                             labels within this range
   3291     text_attr               default={}      SVG attributes for the text labels
   3292     attribute=value pairs   keyword list    SVG attributes for all lines
   3293 
   3294     The exclude option is provided for Axes to keep text from overlapping
   3295     where the axes cross. Normal users are not likely to need it.
   3296     """
   3297     defaults = {"stroke-width": "0.25pt", }
   3298     text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, "text-anchor": "end", "dominant-baseline": "middle", }
   3299     text_start = 2.5
   3300     text_angle = 90.
   3301 
   3302     def __repr__(self):
   3303         return "<YAxis (%g, %g) at x=%g ticks=%s labels=%s %s>" % (
   3304                self.ymin, self.ymax, self.atx, str(self.ticks), str(self.labels), self.attr) # XXX self.ymin/ymax undefd!
   3305 
   3306     def __init__(self, ymin, ymax, atx=0, ticks=-10, miniticks=True, labels=True, logbase=None,
   3307                  arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
   3308         self.atx = atx
   3309         tattr = dict(self.text_defaults)
   3310         tattr.update(text_attr)
   3311         LineAxis.__init__(self, atx, ymin, atx, ymax, ymin, ymax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
   3312 
   3313     def SVG(self, trans=None):
   3314         """Apply the transformation "trans" and return an SVG object."""
   3315         self.x1 = self.atx
   3316         self.x2 = self.atx
   3317         return LineAxis.SVG(self, trans)
   3318 
   3319 
   3320 class Axes:
   3321     """Draw a pair of intersecting x-y axes.
   3322 
   3323     Axes(xmin, xmax, ymin, ymax, atx, aty, xticks, xminiticks, xlabels, xlogbase,
   3324     yticks, yminiticks, ylabels, ylogbase, arrows, text_attr, attribute=value)
   3325 
   3326     xmin, xmax               required       the x range
   3327     ymin, ymax               required       the y range
   3328     atx, aty                 default=0, 0   point where the axes try to cross;
   3329                                             if outside the range, the axes will
   3330                                             cross at the closest corner
   3331     xticks                   default=-10    request ticks according to the standard
   3332                                             tick specification (see help(Ticks))
   3333     xminiticks               default=True   request miniticks according to the
   3334                                             standard minitick specification
   3335     xlabels                  True           request tick labels according to the
   3336                                             standard tick label specification
   3337     xlogbase                 default=None   if a number, the x axis is logarithmic
   3338                                             with ticks at the given base (usually 10)
   3339     yticks                   default=-10    request ticks according to the standard
   3340                                             tick specification
   3341     yminiticks               default=True   request miniticks according to the
   3342                                             standard minitick specification
   3343     ylabels                  True           request tick labels according to the
   3344                                             standard tick label specification
   3345     ylogbase                 default=None   if a number, the y axis is logarithmic
   3346                                             with ticks at the given base (usually 10)
   3347     arrows                   default=None   if a new string identifier, draw arrows
   3348                                             referenced by that identifier
   3349     text_attr                default={}     SVG attributes for the text labels
   3350     attribute=value pairs    keyword list   SVG attributes for all lines
   3351     """
   3352     defaults = {"stroke-width": "0.25pt", }
   3353     text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
   3354 
   3355     def __repr__(self):
   3356         return "<Axes x=(%g, %g) y=(%g, %g) at (%g, %g) %s>" % (
   3357                self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty, self.attr)
   3358 
   3359     def __init__(self, xmin, xmax, ymin, ymax, atx=0, aty=0,
   3360                  xticks=-10, xminiticks=True, xlabels=True, xlogbase=None,
   3361                  yticks=-10, yminiticks=True, ylabels=True, ylogbase=None,
   3362                  arrows=None, text_attr={}, **attr):
   3363         self.xmin, self.xmax = xmin, xmax
   3364         self.ymin, self.ymax = ymin, ymax
   3365         self.atx, self.aty = atx, aty
   3366         self.xticks, self.xminiticks, self.xlabels, self.xlogbase = xticks, xminiticks, xlabels, xlogbase
   3367         self.yticks, self.yminiticks, self.ylabels, self.ylogbase = yticks, yminiticks, ylabels, ylogbase
   3368         self.arrows = arrows
   3369 
   3370         self.text_attr = dict(self.text_defaults)
   3371         self.text_attr.update(text_attr)
   3372 
   3373         self.attr = dict(self.defaults)
   3374         self.attr.update(attr)
   3375 
   3376     def SVG(self, trans=None):
   3377         """Apply the transformation "trans" and return an SVG object."""
   3378         atx, aty = self.atx, self.aty
   3379         if atx < self.xmin:
   3380             atx = self.xmin
   3381         if atx > self.xmax:
   3382             atx = self.xmax
   3383         if aty < self.ymin:
   3384             aty = self.ymin
   3385         if aty > self.ymax:
   3386             aty = self.ymax
   3387 
   3388         xmargin = 0.1 * abs(self.ymin - self.ymax)
   3389         xexclude = atx - xmargin, atx + xmargin
   3390 
   3391         ymargin = 0.1 * abs(self.xmin - self.xmax)
   3392         yexclude = aty - ymargin, aty + ymargin
   3393 
   3394         if self.arrows is not None and self.arrows != False:
   3395             xarrow_start = self.arrows + ".xstart"
   3396             xarrow_end = self.arrows + ".xend"
   3397             yarrow_start = self.arrows + ".ystart"
   3398             yarrow_end = self.arrows + ".yend"
   3399         else:
   3400             xarrow_start = xarrow_end = yarrow_start = yarrow_end = None
   3401 
   3402         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)
   3403         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)
   3404         return SVG("g", *(xaxis.sub + yaxis.sub))
   3405 
   3406 ######################################################################
   3407 
   3408 class HGrid(Ticks):
   3409     """Draws the horizontal lines of a grid over a specified region
   3410     using the standard tick specification (see help(Ticks)) to place the
   3411     grid lines.
   3412 
   3413     HGrid(xmin, xmax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
   3414 
   3415     xmin, xmax              required        the x range
   3416     low, high               required        the y range
   3417     ticks                   default=-10     request ticks according to the standard
   3418                                             tick specification (see help(Ticks))
   3419     miniticks               default=False   request miniticks according to the
   3420                                             standard minitick specification
   3421     logbase                 default=None    if a number, the axis is logarithmic
   3422                                             with ticks at the given base (usually 10)
   3423     mini_attr               default={}      SVG attributes for the minitick-lines
   3424                                             (if miniticks != False)
   3425     attribute=value pairs   keyword list    SVG attributes for the major tick lines
   3426     """
   3427     defaults = {"stroke-width": "0.25pt", "stroke": "gray", }
   3428     mini_defaults = {"stroke-width": "0.25pt", "stroke": "lightgray", "stroke-dasharray": "1,1", }
   3429 
   3430     def __repr__(self):
   3431         return "<HGrid x=(%g, %g) %g <= y <= %g ticks=%s miniticks=%s %s>" % (
   3432                self.xmin, self.xmax, self.low, self.high, str(self.ticks), str(self.miniticks), self.attr)
   3433 
   3434     def __init__(self, xmin, xmax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
   3435         self.xmin, self.xmax = xmin, xmax
   3436 
   3437         self.mini_attr = dict(self.mini_defaults)
   3438         self.mini_attr.update(mini_attr)
   3439 
   3440         Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
   3441 
   3442         self.attr = dict(self.defaults)
   3443         self.attr.update(attr)
   3444 
   3445     def SVG(self, trans=None):
   3446         """Apply the transformation "trans" and return an SVG object."""
   3447         self.last_ticks, self.last_miniticks = Ticks.interpret(self)
   3448 
   3449         ticksd = []
   3450         for t in self.last_ticks.keys():
   3451             ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
   3452 
   3453         miniticksd = []
   3454         for t in self.last_miniticks:
   3455             miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
   3456 
   3457         return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
   3458 
   3459 
   3460 class VGrid(Ticks):
   3461     """Draws the vertical lines of a grid over a specified region
   3462     using the standard tick specification (see help(Ticks)) to place the
   3463     grid lines.
   3464 
   3465     HGrid(ymin, ymax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
   3466 
   3467     ymin, ymax              required        the y range
   3468     low, high               required        the x range
   3469     ticks                   default=-10     request ticks according to the standard
   3470                                             tick specification (see help(Ticks))
   3471     miniticks               default=False   request miniticks according to the
   3472                                             standard minitick specification
   3473     logbase                 default=None    if a number, the axis is logarithmic
   3474                                             with ticks at the given base (usually 10)
   3475     mini_attr               default={}      SVG attributes for the minitick-lines
   3476                                             (if miniticks != False)
   3477     attribute=value pairs   keyword list    SVG attributes for the major tick lines
   3478     """
   3479     defaults = {"stroke-width": "0.25pt", "stroke": "gray", }
   3480     mini_defaults = {"stroke-width": "0.25pt", "stroke": "lightgray", "stroke-dasharray": "1,1", }
   3481 
   3482     def __repr__(self):
   3483         return "<VGrid y=(%g, %g) %g <= x <= %g ticks=%s miniticks=%s %s>" % (
   3484                self.ymin, self.ymax, self.low, self.high, str(self.ticks), str(self.miniticks), self.attr)
   3485 
   3486     def __init__(self, ymin, ymax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
   3487         self.ymin, self.ymax = ymin, ymax
   3488 
   3489         self.mini_attr = dict(self.mini_defaults)
   3490         self.mini_attr.update(mini_attr)
   3491 
   3492         Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
   3493 
   3494         self.attr = dict(self.defaults)
   3495         self.attr.update(attr)
   3496 
   3497     def SVG(self, trans=None):
   3498         """Apply the transformation "trans" and return an SVG object."""
   3499         self.last_ticks, self.last_miniticks = Ticks.interpret(self)
   3500 
   3501         ticksd = []
   3502         for t in self.last_ticks.keys():
   3503             ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
   3504 
   3505         miniticksd = []
   3506         for t in self.last_miniticks:
   3507             miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
   3508 
   3509         return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
   3510 
   3511 
   3512 class Grid(Ticks):
   3513     """Draws a grid over a specified region using the standard tick
   3514     specification (see help(Ticks)) to place the grid lines.
   3515 
   3516     Grid(xmin, xmax, ymin, ymax, ticks, miniticks, logbase, mini_attr, attribute=value)
   3517 
   3518     xmin, xmax              required        the x range
   3519     ymin, ymax              required        the y range
   3520     ticks                   default=-10     request ticks according to the standard
   3521                                             tick specification (see help(Ticks))
   3522     miniticks               default=False   request miniticks according to the
   3523                                             standard minitick specification
   3524     logbase                 default=None    if a number, the axis is logarithmic
   3525                                             with ticks at the given base (usually 10)
   3526     mini_attr               default={}      SVG attributes for the minitick-lines
   3527                                             (if miniticks != False)
   3528     attribute=value pairs   keyword list    SVG attributes for the major tick lines
   3529     """
   3530     defaults = {"stroke-width": "0.25pt", "stroke": "gray", }
   3531     mini_defaults = {"stroke-width": "0.25pt", "stroke": "lightgray", "stroke-dasharray": "1,1", }
   3532 
   3533     def __repr__(self):
   3534         return "<Grid x=(%g, %g) y=(%g, %g) ticks=%s miniticks=%s %s>" % (
   3535                self.xmin, self.xmax, self.ymin, self.ymax, str(self.ticks), str(self.miniticks), self.attr)
   3536 
   3537     def __init__(self, xmin, xmax, ymin, ymax, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
   3538         self.xmin, self.xmax = xmin, xmax
   3539         self.ymin, self.ymax = ymin, ymax
   3540 
   3541         self.mini_attr = dict(self.mini_defaults)
   3542         self.mini_attr.update(mini_attr)
   3543 
   3544         Ticks.__init__(self, None, None, None, ticks, miniticks, None, logbase)
   3545 
   3546         self.attr = dict(self.defaults)
   3547         self.attr.update(attr)
   3548 
   3549     def SVG(self, trans=None):
   3550         """Apply the transformation "trans" and return an SVG object."""
   3551         self.low, self.high = self.xmin, self.xmax
   3552         self.last_xticks, self.last_xminiticks = Ticks.interpret(self)
   3553         self.low, self.high = self.ymin, self.ymax
   3554         self.last_yticks, self.last_yminiticks = Ticks.interpret(self)
   3555 
   3556         ticksd = []
   3557         for t in self.last_xticks.keys():
   3558             ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
   3559         for t in self.last_yticks.keys():
   3560             ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
   3561 
   3562         miniticksd = []
   3563         for t in self.last_xminiticks:
   3564             miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
   3565         for t in self.last_yminiticks:
   3566             miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
   3567 
   3568         return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
   3569 
   3570 ######################################################################
   3571 
   3572 class XErrorBars:
   3573     """Draws x error bars at a set of points. This is usually used
   3574     before (under) a set of Dots at the same points.
   3575 
   3576     XErrorBars(d, attribute=value)
   3577 
   3578     d                       required        list of (x,y,xerr...) points
   3579     attribute=value pairs   keyword list    SVG attributes
   3580 
   3581     If points in d have
   3582 
   3583         * 3 elements, the third is the symmetric error bar
   3584         * 4 elements, the third and fourth are the asymmetric lower and
   3585           upper error bar. The third element should be negative,
   3586           e.g. (5, 5, -1, 2) is a bar from 4 to 7.
   3587         * more than 4, a tick mark is placed at each value. This lets
   3588           you nest errors from different sources, correlated and
   3589           uncorrelated, statistical and systematic, etc.
   3590     """
   3591     defaults = {"stroke-width": "0.25pt", }
   3592 
   3593     def __repr__(self):
   3594         return "<XErrorBars (%d nodes)>" % len(self.d)
   3595 
   3596     def __init__(self, d=[], **attr):
   3597         self.d = list(d)
   3598 
   3599         self.attr = dict(self.defaults)
   3600         self.attr.update(attr)
   3601 
   3602     def SVG(self, trans=None):
   3603         """Apply the transformation "trans" and return an SVG object."""
   3604         if isinstance(trans, basestring):
   3605             trans = totrans(trans) # only once
   3606 
   3607         output = SVG("g")
   3608         for p in self.d:
   3609             x, y = p[0], p[1]
   3610 
   3611             if len(p) == 3:
   3612                 bars = [x - p[2], x + p[2]]
   3613             else:
   3614                 bars = [x + pi for pi in p[2:]]
   3615 
   3616             start, end = min(bars), max(bars)
   3617             output.append(LineAxis(start, y, end, y, start, end, bars, False, False, **self.attr).SVG(trans))
   3618 
   3619         return output
   3620 
   3621 
   3622 class YErrorBars:
   3623     """Draws y error bars at a set of points. This is usually used
   3624     before (under) a set of Dots at the same points.
   3625 
   3626     YErrorBars(d, attribute=value)
   3627 
   3628     d                       required        list of (x,y,yerr...) points
   3629     attribute=value pairs   keyword list    SVG attributes
   3630 
   3631     If points in d have
   3632 
   3633         * 3 elements, the third is the symmetric error bar
   3634         * 4 elements, the third and fourth are the asymmetric lower and
   3635           upper error bar. The third element should be negative,
   3636           e.g. (5, 5, -1, 2) is a bar from 4 to 7.
   3637         * more than 4, a tick mark is placed at each value. This lets
   3638           you nest errors from different sources, correlated and
   3639           uncorrelated, statistical and systematic, etc.
   3640     """
   3641     defaults = {"stroke-width": "0.25pt", }
   3642 
   3643     def __repr__(self):
   3644         return "<YErrorBars (%d nodes)>" % len(self.d)
   3645 
   3646     def __init__(self, d=[], **attr):
   3647         self.d = list(d)
   3648 
   3649         self.attr = dict(self.defaults)
   3650         self.attr.update(attr)
   3651 
   3652     def SVG(self, trans=None):
   3653         """Apply the transformation "trans" and return an SVG object."""
   3654         if isinstance(trans, basestring):
   3655             trans = totrans(trans) # only once
   3656 
   3657         output = SVG("g")
   3658         for p in self.d:
   3659             x, y = p[0], p[1]
   3660 
   3661             if len(p) == 3:
   3662                 bars = [y - p[2], y + p[2]]
   3663             else:
   3664                 bars = [y + pi for pi in p[2:]]
   3665 
   3666             start, end = min(bars), max(bars)
   3667             output.append(LineAxis(x, start, x, end, start, end, bars, False, False, **self.attr).SVG(trans))
   3668 
   3669         return output
   3670