1#!/usr/bin/env python
2
3# svgfig.py copyright (C) 2008 Jim Pivarski <jpivarski@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
21import re, codecs, os, platform, copy, itertools, math, cmath, random, sys, copy
22_epsilon = 1e-5
23
24
25if 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
42def 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
51def 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
68class 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
527def 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
555def 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
567def 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
593def load(fileName):
594    """Loads an SVG image from a file."""
595    return load_stream(file(fileName))
596
597def 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######################################################################
645def 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
653def 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
702def 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
770def 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
777class 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
857class 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
957class 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
1101def 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
1116class 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
1570def 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
1591def 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
1610def 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
1629class 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
1832class 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
2025class 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
2056class 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
2087def 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
2103class 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
2165def 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
2179class 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
2254class 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
2336class 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
2365class 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
2395class 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
2451class 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
2500def 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
2544class 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
3057class 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
3121class 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
3210class 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
3265class 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
3320class 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
3408class 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
3460class 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
3512class 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
3572class 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
3622class 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