From e7f0ad3c180e707d375cd7bad436fbceb8f517ed Mon Sep 17 00:00:00 2001 From: Ethan Rublee Date: Wed, 23 Feb 2011 18:11:25 +0000 Subject: [PATCH] Command line python tools for generating patterns, may be incomplete. --- doc/pattern_tools/README.txt | 4 + doc/pattern_tools/gen_pattern.py | 121 + doc/pattern_tools/svgfig.py | 3668 ++++++++++++++++++++++++++++++ 3 files changed, 3793 insertions(+) create mode 100644 doc/pattern_tools/README.txt create mode 100644 doc/pattern_tools/gen_pattern.py create mode 100644 doc/pattern_tools/svgfig.py diff --git a/doc/pattern_tools/README.txt b/doc/pattern_tools/README.txt new file mode 100644 index 0000000000..dfe279022b --- /dev/null +++ b/doc/pattern_tools/README.txt @@ -0,0 +1,4 @@ +use +python gen_pattern.py --help + +to generate various calibration svg calibration patterns. diff --git a/doc/pattern_tools/gen_pattern.py b/doc/pattern_tools/gen_pattern.py new file mode 100644 index 0000000000..7d415ac12e --- /dev/null +++ b/doc/pattern_tools/gen_pattern.py @@ -0,0 +1,121 @@ +"""gen_pattern.py +To run: +-c 10 -r 12 -o out.svg +-T type of pattern, circles, acircles, checkerboard +-s --square_size size of squares in pattern +-u --units mm, inches, px, m +-w page width in units +-h page height in units +""" + +from svgfig import * + +import sys +import getopt + +class PatternMaker: + def __init__(self, cols,rows,output,units,square_size,page_width,page_height): + self.cols = cols + self.rows = rows + self.output = output + self.units = units + self.square_size = square_size + self.width = page_width + self.height = page_height + self.g = SVG("g") # the svg group container + def makeCirclesPattern(self): + spacing = self.square_size + r = spacing / 5.0 #radius is a 5th of the spacing TODO parameterize + for x in range(1,self.cols+1): + for y in range(1,self.rows+1): + dot = SVG("circle", cx=x * spacing, cy=y * spacing, r=r, fill="black") + self.g.append(dot) + + def makeACirclesPattern(self): + spacing = self.square_size + r = spacing / 5.0 + row_shift = 0 + for y in range(1,self.rows+1): + for x in range(1,self.cols+1): + dot = SVG("circle", cx= (x + row_shift/2.0) * spacing, cy=y * spacing, r=r, fill="black") + self.g.append(dot) + #this makes the pattern asymetric + if row_shift is 0: + row_shift = 1 + else: + row_shift = 0 + def makeCheckerboardPattern(self): + spacing = self.square_size + r = spacing / 5.0 + for x in range(1,self.cols+1): + for y in range(1,self.rows+1): + #TODO make a checkerboard pattern + dot = SVG("circle", cx=x * spacing, cy=y * spacing, r=r, fill="black") + self.g.append(dot) + def save(self): + c = canvas(self.g,width="%d%s"%(self.width,self.units),height="%d%s"%(self.height,self.units),viewBox="0 0 %d %d"%(self.width,self.height)) + c.inkview(self.output) + +def makePattern(cols,rows,output,p_type,units,square_size,page_width,page_height): + width = page_width + spacing = square_size + height = page_height + r = spacing / 5.0 + g = SVG("g") # the svg group container + for x in range(1,cols+1): + for y in range(1,rows+1): + if "circle" in p_type: + dot = SVG("circle", cx=x * spacing, cy=y * spacing, r=r, fill="black") + g.append(dot) + c = canvas(g,width="%d%s"%(width,units),height="%d%s"%(height,units),viewBox="0 0 %d %d"%(width,height)) + c.inkview(output) + + +def main(): + # parse command line options, TODO use argparse for better doc + try: + opts, args = getopt.getopt(sys.argv[1:], "ho:c:r:T:u:s:w:h:", ["help","output","columns","rows", + "type","units","square_size","page_width", + "page_height"]) + except getopt.error, msg: + print msg + print "for help use --help" + sys.exit(2) + output = "out.svg" + columns = 8 + rows = 11 + p_type = "circles" + units = "mm" + square_size = 20.0 + page_width = 216 #8.5 inches + page_height = 279 #11 inches + # process options + for o, a in opts: + if o in ("-h", "--help"): + print __doc__ + sys.exit(0) + elif o in ("-r", "--rows"): + rows = int(a) + elif o in ("-c", "--columns"): + columns = int(a) + elif o in ("-o", "--output"): + output = a + elif o in ("-T", "--type"): + p_type = a + elif o in ("-u", "--units"): + units = a + elif o in ("-s", "--square_size"): + square_size = float(a) + elif o in ("-w", "--page_width"): + page_width = float(a) + elif o in ("-h", "--page_height"): + page_height = float(a) + pm = PatternMaker(columns,rows,output,units,square_size,page_width,page_height) + #dict for easy lookup of pattern type + mp = {"circles":pm.makeCirclesPattern,"acircles":pm.makeACirclesPattern,"checkerboard":pm.makeCheckerboardPattern} + mp[p_type]() + #this should save pattern to output + pm.save() + +if __name__ == "__main__": + main() diff --git a/doc/pattern_tools/svgfig.py b/doc/pattern_tools/svgfig.py new file mode 100644 index 0000000000..04aa257357 --- /dev/null +++ b/doc/pattern_tools/svgfig.py @@ -0,0 +1,3668 @@ +# svgfig.py copyright (C) 2008 Jim Pivarski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +# +# Full licence is in the file COPYING and at http://www.gnu.org/copyleft/gpl.html + +import re, codecs, os, platform, copy, itertools, math, cmath, random, sys, copy +_epsilon = 1e-5 + + +if re.search("windows", platform.system(), re.I): + try: + import _winreg + _default_directory = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\Current Version\Explorer\Shell Folders"), "Desktop")[0] +# tmpdir = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Environment"), "TEMP")[0] +# if tmpdir[0:13] != "%USERPROFILE%": +# tmpdir = os.path.expanduser("~") + tmpdir[13:] + except: + _default_directory = os.path.expanduser("~") + os.sep + "Desktop" + +_default_fileName = "tmp.svg" + +_hacks = {} +_hacks["inkscape-text-vertical-shift"] = False + + +def rgb(r, g, b, maximum=1.): + """Create an SVG color string "#xxyyzz" from r, g, and b. + + r,g,b = 0 is black and r,g,b = maximum is white. + """ + return "#%02x%02x%02x" % (max(0, min(r*255./maximum, 255)), + max(0, min(g*255./maximum, 255)), + max(0, min(b*255./maximum, 255))) + +def attr_preprocess(attr): + for name in attr.keys(): + name_colon = re.sub("__", ":", name) + if name_colon != name: + attr[name_colon] = attr[name] + del attr[name] + name = name_colon + + name_dash = re.sub("_", "-", name) + if name_dash != name: + attr[name_dash] = attr[name] + del attr[name] + name = name_dash + + return attr + + +class SVG: + """A tree representation of an SVG image or image fragment. + + SVG(t, sub, sub, sub..., attribute=value) + + t required SVG type name + sub optional list nested SVG elements or text/Unicode + attribute=value pairs optional keywords SVG attributes + + In attribute names, "__" becomes ":" and "_" becomes "-". + + SVG in XML + + + + + + + SVG in Python + + >>> svg = SVG("g", SVG("rect", x=1, y=1, width=2, height=2), \ + ... SVG("rect", x=3, y=3, width=2, height=2), \ + ... id="mygroup", fill="blue") + + Sub-elements and attributes may be accessed through tree-indexing: + + >>> svg = SVG("text", SVG("tspan", "hello there"), stroke="none", fill="black") + >>> svg[0] + + >>> svg[0, 0] + 'hello there' + >>> svg["fill"] + 'black' + + Iteration is depth-first: + + >>> svg = SVG("g", SVG("g", SVG("line", x1=0, y1=0, x2=1, y2=1)), \ + ... SVG("text", SVG("tspan", "hello again"))) + ... + >>> for ti, s in svg: + ... print ti, repr(s) + ... + (0,) + (0, 0) + (0, 0, 'x2') 1 + (0, 0, 'y1') 0 + (0, 0, 'x1') 0 + (0, 0, 'y2') 1 + (1,) + (1, 0) + (1, 0, 0) 'hello again' + + Use "print" to navigate: + + >>> print svg + None + [0] + [0, 0] + [1] + [1, 0] + """ + def __init__(self, *t_sub, **attr): + if len(t_sub) == 0: + raise TypeError, "SVG element must have a t (SVG type)" + + # first argument is t (SVG type) + self.t = t_sub[0] + # the rest are sub-elements + self.sub = list(t_sub[1:]) + + # keyword arguments are attributes + # need to preprocess to handle differences between SVG and Python syntax + self.attr = attr_preprocess(attr) + + def __getitem__(self, ti): + """Index is a list that descends tree, returning a sub-element if + it ends with a number and an attribute if it ends with a string.""" + obj = self + if isinstance(ti, (list, tuple)): + for i in ti[:-1]: + obj = obj[i] + ti = ti[-1] + + if isinstance(ti, (int, long, slice)): + return obj.sub[ti] + else: + return obj.attr[ti] + + def __setitem__(self, ti, value): + """Index is a list that descends tree, returning a sub-element if + it ends with a number and an attribute if it ends with a string.""" + obj = self + if isinstance(ti, (list, tuple)): + for i in ti[:-1]: + obj = obj[i] + ti = ti[-1] + + if isinstance(ti, (int, long, slice)): + obj.sub[ti] = value + else: + obj.attr[ti] = value + + def __delitem__(self, ti): + """Index is a list that descends tree, returning a sub-element if + it ends with a number and an attribute if it ends with a string.""" + obj = self + if isinstance(ti, (list, tuple)): + for i in ti[:-1]: + obj = obj[i] + ti = ti[-1] + + if isinstance(ti, (int, long, slice)): + del obj.sub[ti] + else: + del obj.attr[ti] + + def __contains__(self, value): + """x in svg == True iff x is an attribute in svg.""" + return value in self.attr + + def __eq__(self, other): + """x == y iff x represents the same SVG as y.""" + if id(self) == id(other): + return True + return (isinstance(other, SVG) and + self.t == other.t and self.sub == other.sub and self.attr == other.attr) + + def __ne__(self, other): + """x != y iff x does not represent the same SVG as y.""" + return not (self == other) + + def append(self, x): + """Appends x to the list of sub-elements (drawn last, overlaps + other primitives).""" + self.sub.append(x) + + def prepend(self, x): + """Prepends x to the list of sub-elements (drawn first may be + overlapped by other primitives).""" + self.sub[0:0] = [x] + + def extend(self, x): + """Extends list of sub-elements by a list x.""" + self.sub.extend(x) + + def clone(self, shallow=False): + """Deep copy of SVG tree. Set shallow=True for a shallow copy.""" + if shallow: + return copy.copy(self) + else: + return copy.deepcopy(self) + + ### nested class + class SVGDepthIterator: + """Manages SVG iteration.""" + + def __init__(self, svg, ti, depth_limit): + self.svg = svg + self.ti = ti + self.shown = False + self.depth_limit = depth_limit + + def __iter__(self): + return self + + def next(self): + if not self.shown: + self.shown = True + if self.ti != (): + return self.ti, self.svg + + if not isinstance(self.svg, SVG): + raise StopIteration + if self.depth_limit is not None and len(self.ti) >= self.depth_limit: + raise StopIteration + + if "iterators" not in self.__dict__: + self.iterators = [] + for i, s in enumerate(self.svg.sub): + self.iterators.append(self.__class__(s, self.ti + (i,), self.depth_limit)) + for k, s in self.svg.attr.items(): + self.iterators.append(self.__class__(s, self.ti + (k,), self.depth_limit)) + self.iterators = itertools.chain(*self.iterators) + + return self.iterators.next() + ### end nested class + + def depth_first(self, depth_limit=None): + """Returns a depth-first generator over the SVG. If depth_limit + is a number, stop recursion at that depth.""" + return self.SVGDepthIterator(self, (), depth_limit) + + def breadth_first(self, depth_limit=None): + """Not implemented yet. Any ideas on how to do it? + + Returns a breadth-first generator over the SVG. If depth_limit + is a number, stop recursion at that depth.""" + raise NotImplementedError, "Got an algorithm for breadth-first searching a tree without effectively copying the tree?" + + def __iter__(self): + return self.depth_first() + + def items(self, sub=True, attr=True, text=True): + """Get a recursively-generated list of tree-index, sub-element/attribute pairs. + + If sub == False, do not show sub-elements. + If attr == False, do not show attributes. + If text == False, do not show text/Unicode sub-elements. + """ + output = [] + for ti, s in self: + show = False + if isinstance(ti[-1], (int, long)): + if isinstance(s, basestring): + show = text + else: + show = sub + else: + show = attr + + if show: + output.append((ti, s)) + return output + + def keys(self, sub=True, attr=True, text=True): + """Get a recursively-generated list of tree-indexes. + + If sub == False, do not show sub-elements. + If attr == False, do not show attributes. + If text == False, do not show text/Unicode sub-elements. + """ + return [ti for ti, s in self.items(sub, attr, text)] + + def values(self, sub=True, attr=True, text=True): + """Get a recursively-generated list of sub-elements and attributes. + + If sub == False, do not show sub-elements. + If attr == False, do not show attributes. + If text == False, do not show text/Unicode sub-elements. + """ + return [s for ti, s in self.items(sub, attr, text)] + + def __repr__(self): + return self.xml(depth_limit=0) + + def __str__(self): + """Print (actually, return a string of) the tree in a form useful for browsing.""" + return self.tree(sub=True, attr=False, text=False) + + def tree(self, depth_limit=None, sub=True, attr=True, text=True, tree_width=20, obj_width=80): + """Print (actually, return a string of) the tree in a form useful for browsing. + + If depth_limit == a number, stop recursion at that depth. + If sub == False, do not show sub-elements. + If attr == False, do not show attributes. + If text == False, do not show text/Unicode sub-elements. + tree_width is the number of characters reserved for printing tree indexes. + obj_width is the number of characters reserved for printing sub-elements/attributes. + """ + output = [] + + line = "%s %s" % (("%%-%ds" % tree_width) % repr(None), + ("%%-%ds" % obj_width) % (repr(self))[0:obj_width]) + output.append(line) + + for ti, s in self.depth_first(depth_limit): + show = False + if isinstance(ti[-1], (int, long)): + if isinstance(s, basestring): + show = text + else: + show = sub + else: + show = attr + + if show: + line = "%s %s" % (("%%-%ds" % tree_width) % repr(list(ti)), + ("%%-%ds" % obj_width) % (" "*len(ti) + repr(s))[0:obj_width]) + output.append(line) + + return "\n".join(output) + + def xml(self, indent=u" ", newl=u"\n", depth_limit=None, depth=0): + """Get an XML representation of the SVG. + + indent string used for indenting + newl string used for newlines + If depth_limit == a number, stop recursion at that depth. + depth starting depth (not useful for users) + + print svg.xml() + """ + attrstr = [] + for n, v in self.attr.items(): + if isinstance(v, dict): + v = u"; ".join([u"%s:%s" % (ni, vi) for ni, vi in v.items()]) + elif isinstance(v, (list, tuple)): + v = u", ".join(v) + attrstr.append(u" %s=%s" % (n, repr(v))) + attrstr = u"".join(attrstr) + + if len(self.sub) == 0: + return u"%s<%s%s />" % (indent * depth, self.t, attrstr) + + if depth_limit is None or depth_limit > depth: + substr = [] + for s in self.sub: + if isinstance(s, SVG): + substr.append(s.xml(indent, newl, depth_limit, depth + 1) + newl) + elif isinstance(s, basestring): + substr.append(u"%s%s%s" % (indent * (depth + 1), s, newl)) + else: + substr.append("%s%s%s" % (indent * (depth + 1), repr(s), newl)) + substr = u"".join(substr) + + return u"%s<%s%s>%s%s%s" % (indent * depth, self.t, attrstr, newl, substr, indent * depth, self.t) + + else: + return u"%s<%s (%d sub)%s />" % (indent * depth, self.t, len(self.sub), attrstr) + + def standalone_xml(self, indent=u" ", newl=u"\n", encoding=u"utf-8"): + """Get an XML representation of the SVG that can be saved/rendered. + + indent string used for indenting + newl string used for newlines + """ + + if self.t == "svg": + top = self + else: + top = canvas(self) + return u"""\ + + + +""" % encoding + (u"".join(top.__standalone_xml(indent, newl))) # end of return statement + + def __standalone_xml(self, indent, newl): + output = [u"<%s" % self.t] + + for n, v in self.attr.items(): + if isinstance(v, dict): + v = u"; ".join([u"%s:%s" % (ni, vi) for ni, vi in v.items()]) + elif isinstance(v, (list, tuple)): + v = u", ".join(v) + output.append(u' %s="%s"' % (n, v)) + + if len(self.sub) == 0: + output.append(u" />%s%s" % (newl, newl)) + return output + + elif self.t == "text" or self.t == "tspan" or self.t == "style": + output.append(u">") + + else: + output.append(u">%s%s" % (newl, newl)) + + for s in self.sub: + if isinstance(s, SVG): + output.extend(s.__standalone_xml(indent, newl)) + else: + output.append(unicode(s)) + + if self.t == "tspan": + output.append(u"" % self.t) + else: + output.append(u"%s%s" % (self.t, newl, newl)) + + return output + + def interpret_fileName(self, fileName=None): + if fileName is None: + fileName = _default_fileName + if re.search("windows", platform.system(), re.I) and not os.path.isabs(fileName): + fileName = _default_directory + os.sep + fileName + return fileName + + def save(self, fileName=None, encoding="utf-8", compresslevel=None): + """Save to a file for viewing. Note that svg.save() overwrites the file named _default_fileName. + + fileName default=None note that _default_fileName will be overwritten if + no fileName is specified. If the extension + is ".svgz" or ".gz", the output will be gzipped + encoding default="utf-8" file encoding + compresslevel default=None if a number, the output will be gzipped with that + compression level (1-9, 1 being fastest and 9 most + thorough) + """ + fileName = self.interpret_fileName(fileName) + + if compresslevel is not None or re.search(r"\.svgz$", fileName, re.I) or re.search(r"\.gz$", fileName, re.I): + import gzip + if compresslevel is None: + f = gzip.GzipFile(fileName, "w") + else: + f = gzip.GzipFile(fileName, "w", compresslevel) + + f = codecs.EncodedFile(f, "utf-8", encoding) + f.write(self.standalone_xml(encoding=encoding)) + f.close() + + else: + f = codecs.open(fileName, "w", encoding=encoding) + f.write(self.standalone_xml(encoding=encoding)) + f.close() + + def inkview(self, fileName=None, encoding="utf-8"): + """View in "inkview", assuming that program is available on your system. + + fileName default=None note that any file named _default_fileName will be + overwritten if no fileName is specified. If the extension + is ".svgz" or ".gz", the output will be gzipped + encoding default="utf-8" file encoding + """ + fileName = self.interpret_fileName(fileName) + self.save(fileName, encoding) + os.spawnvp(os.P_NOWAIT, "inkview", ("inkview", fileName)) + + def inkscape(self, fileName=None, encoding="utf-8"): + """View in "inkscape", assuming that program is available on your system. + + fileName default=None note that any file named _default_fileName will be + overwritten if no fileName is specified. If the extension + is ".svgz" or ".gz", the output will be gzipped + encoding default="utf-8" file encoding + """ + fileName = self.interpret_fileName(fileName) + self.save(fileName, encoding) + os.spawnvp(os.P_NOWAIT, "inkscape", ("inkscape", fileName)) + + def firefox(self, fileName=None, encoding="utf-8"): + """View in "firefox", assuming that program is available on your system. + + fileName default=None note that any file named _default_fileName will be + overwritten if no fileName is specified. If the extension + is ".svgz" or ".gz", the output will be gzipped + encoding default="utf-8" file encoding + """ + fileName = self.interpret_fileName(fileName) + self.save(fileName, encoding) + os.spawnvp(os.P_NOWAIT, "firefox", ("firefox", fileName)) + +###################################################################### + +_canvas_defaults = {"width": "400px", + "height": "400px", + "viewBox": "0 0 100 100", + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + "version": "1.1", + "style": {"stroke": "black", + "fill": "none", + "stroke-width": "0.5pt", + "stroke-linejoin": "round", + "text-anchor": "middle", + }, + "font-family": ["Helvetica", "Arial", "FreeSans", "Sans", "sans", "sans-serif"], + } + +def canvas(*sub, **attr): + """Creates a top-level SVG object, allowing the user to control the + image size and aspect ratio. + + canvas(sub, sub, sub..., attribute=value) + + sub optional list nested SVG elements or text/Unicode + attribute=value pairs optional keywords SVG attributes + + Default attribute values: + + width "400px" + height "400px" + viewBox "0 0 100 100" + xmlns "http://www.w3.org/2000/svg" + xmlns:xlink "http://www.w3.org/1999/xlink" + version "1.1" + style "stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoin:round; text-anchor:middle" + font-family "Helvetica,Arial,FreeSans?,Sans,sans,sans-serif" + """ + attributes = dict(_canvas_defaults) + attributes.update(attr) + + if sub is None or sub == (): + return SVG("svg", **attributes) + else: + return SVG("svg", *sub, **attributes) + +def canvas_outline(*sub, **attr): + """Same as canvas(), but draws an outline around the drawable area, + so that you know how close your image is to the edges.""" + svg = canvas(*sub, **attr) + match = re.match(r"[, \t]*([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]*", svg["viewBox"]) + if match is None: + raise ValueError, "canvas viewBox is incorrectly formatted" + x, y, width, height = [float(x) for x in match.groups()] + svg.prepend(SVG("rect", x=x, y=y, width=width, height=height, stroke="none", fill="cornsilk")) + svg.append(SVG("rect", x=x, y=y, width=width, height=height, stroke="black", fill="none")) + return svg + +def template(fileName, svg, replaceme="REPLACEME"): + """Loads an SVG image from a file, replacing instances of + with a given svg object. + + fileName required name of the template SVG + svg required SVG object for replacement + replaceme default="REPLACEME" fake SVG element to be replaced by the given object + + >>> print load("template.svg") + None + >>> + >>> print template("template.svg", SVG("circle", cx=50, cy=50, r=30)) + None + """ + output = load(fileName) + for ti, s in output: + if isinstance(s, SVG) and s.t == replaceme: + output[ti] = svg + return output + +###################################################################### + +def load(fileName): + """Loads an SVG image from a file.""" + return load_stream(file(fileName)) + +def load_stream(stream): + """Loads an SVG image from a stream (can be a string or a file object).""" + + from xml.sax import handler, make_parser + from xml.sax.handler import feature_namespaces, feature_external_ges, feature_external_pes + + class ContentHandler(handler.ContentHandler): + def __init__(self): + self.stack = [] + self.output = None + self.all_whitespace = re.compile(r"^\s*$") + + def startElement(self, name, attr): + s = SVG(name) + s.attr = dict(attr.items()) + if len(self.stack) > 0: + last = self.stack[-1] + last.sub.append(s) + self.stack.append(s) + + def characters(self, ch): + if not isinstance(ch, basestring) or self.all_whitespace.match(ch) is None: + if len(self.stack) > 0: + last = self.stack[-1] + if len(last.sub) > 0 and isinstance(last.sub[-1], basestring): + last.sub[-1] = last.sub[-1] + "\n" + ch + else: + last.sub.append(ch) + + def endElement(self, name): + if len(self.stack) > 0: + last = self.stack[-1] + if (isinstance(last, SVG) and last.t == "style" and + "type" in last.attr and last.attr["type"] == "text/css" and + len(last.sub) == 1 and isinstance(last.sub[0], basestring)): + last.sub[0] = "" + + self.output = self.stack.pop() + + ch = ContentHandler() + parser = make_parser() + parser.setContentHandler(ch) + parser.setFeature(feature_namespaces, 0) + parser.setFeature(feature_external_ges, 0) + parser.parse(stream) + return ch.output + +###################################################################### +def set_func_name(f, name): + """try to patch the function name string into a function object""" + try: + f.func_name = name + except TypeError: + # py 2.3 raises: TypeError: readonly attribute + pass + +def totrans(expr, vars=("x", "y"), globals=None, locals=None): + """Converts to a coordinate transformation (a function that accepts + two arguments and returns two values). + + expr required a string expression or a function + of two real or one complex value + vars default=("x", "y") independent variable names; a singleton + ("z",) is interpreted as complex + globals default=None dict of global variables + locals default=None dict of local variables + """ + if locals is None: + locals = {} # python 2.3's eval() won't accept None + + if callable(expr): + if expr.func_code.co_argcount == 2: + return expr + + elif expr.func_code.co_argcount == 1: + split = lambda z: (z.real, z.imag) + output = lambda x, y: split(expr(x + y*1j)) + set_func_name(output, expr.func_name) + return output + + else: + raise TypeError, "must be a function of 2 or 1 variables" + + if len(vars) == 2: + g = math.__dict__ + if globals is not None: + g.update(globals) + output = eval("lambda %s, %s: (%s)" % (vars[0], vars[1], expr), g, locals) + set_func_name(output, "%s,%s -> %s" % (vars[0], vars[1], expr)) + return output + + elif len(vars) == 1: + g = cmath.__dict__ + if globals is not None: + g.update(globals) + output = eval("lambda %s: (%s)" % (vars[0], expr), g, locals) + split = lambda z: (z.real, z.imag) + output2 = lambda x, y: split(output(x + y*1j)) + set_func_name(output2, "%s -> %s" % (vars[0], expr)) + return output2 + + else: + raise TypeError, "vars must have 2 or 1 elements" + + +def window(xmin, xmax, ymin, ymax, x=0, y=0, width=100, height=100, + xlogbase=None, ylogbase=None, minusInfinity=-1000, flipx=False, flipy=True): + """Creates and returns a coordinate transformation (a function that + accepts two arguments and returns two values) that transforms from + (xmin, ymin), (xmax, ymax) + to + (x, y), (x + width, y + height). + + xlogbase, ylogbase default=None, None if a number, transform + logarithmically with given base + minusInfinity default=-1000 what to return if + log(0 or negative) is attempted + flipx default=False if true, reverse the direction of x + flipy default=True if true, reverse the direction of y + + (When composing windows, be sure to set flipy=False.) + """ + + if flipx: + ox1 = x + width + ox2 = x + else: + ox1 = x + ox2 = x + width + if flipy: + oy1 = y + height + oy2 = y + else: + oy1 = y + oy2 = y + height + ix1 = xmin + iy1 = ymin + ix2 = xmax + iy2 = ymax + + if xlogbase is not None and (ix1 <= 0. or ix2 <= 0.): + raise ValueError, "x range incompatible with log scaling: (%g, %g)" % (ix1, ix2) + + if ylogbase is not None and (iy1 <= 0. or iy2 <= 0.): + raise ValueError, "y range incompatible with log scaling: (%g, %g)" % (iy1, iy2) + + def maybelog(t, it1, it2, ot1, ot2, logbase): + if t <= 0.: + return minusInfinity + else: + return ot1 + 1.*(math.log(t, logbase) - math.log(it1, logbase))/(math.log(it2, logbase) - math.log(it1, logbase)) * (ot2 - ot1) + + xlogstr, ylogstr = "", "" + + if xlogbase is None: + xfunc = lambda x: ox1 + 1.*(x - ix1)/(ix2 - ix1) * (ox2 - ox1) + else: + xfunc = lambda x: maybelog(x, ix1, ix2, ox1, ox2, xlogbase) + xlogstr = " xlog=%g" % xlogbase + + if ylogbase is None: + yfunc = lambda y: oy1 + 1.*(y - iy1)/(iy2 - iy1) * (oy2 - oy1) + else: + yfunc = lambda y: maybelog(y, iy1, iy2, oy1, oy2, ylogbase) + ylogstr = " ylog=%g" % ylogbase + + output = lambda x, y: (xfunc(x), yfunc(y)) + + set_func_name(output, "(%g, %g), (%g, %g) -> (%g, %g), (%g, %g)%s%s" % ( + ix1, ix2, iy1, iy2, ox1, ox2, oy1, oy2, xlogstr, ylogstr)) + return output + + +def rotate(angle, cx=0, cy=0): + """Creates and returns a coordinate transformation which rotates + around (cx,cy) by "angle" degrees.""" + angle *= math.pi/180. + 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)) + + +class Fig: + """Stores graphics primitive objects and applies a single coordinate + transformation to them. To compose coordinate systems, nest Fig + objects. + + Fig(obj, obj, obj..., trans=function) + + obj optional list a list of drawing primitives + trans default=None a coordinate transformation function + + >>> fig = Fig(Line(0,0,1,1), Rect(0.2,0.2,0.8,0.8), trans="2*x, 2*y") + >>> print fig.SVG().xml() + + + + + >>> print Fig(fig, trans="x/2., y/2.").SVG().xml() + + + + + """ + + def __repr__(self): + if self.trans is None: + return "" % len(self.d) + elif isinstance(self.trans, basestring): + return " %s>" % (len(self.d), self.trans) + else: + return "" % (len(self.d), self.trans.func_name) + + def __init__(self, *d, **kwds): + self.d = list(d) + defaults = {"trans": None, } + defaults.update(kwds) + kwds = defaults + + self.trans = kwds["trans"]; del kwds["trans"] + if len(kwds) != 0: + raise TypeError, "Fig() got unexpected keyword arguments %s" % kwds.keys() + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object. + + Coordinate transformations in nested Figs will be composed. + """ + + if trans is None: + trans = self.trans + if isinstance(trans, basestring): + trans = totrans(trans) + + output = SVG("g") + for s in self.d: + if isinstance(s, SVG): + output.append(s) + + elif isinstance(s, Fig): + strans = s.trans + if isinstance(strans, basestring): + strans = totrans(strans) + + if trans is None: + subtrans = strans + elif strans is None: + subtrans = trans + else: + subtrans = lambda x, y: trans(*strans(x, y)) + + output.sub += s.SVG(subtrans).sub + + elif s is None: + pass + + else: + output.append(s.SVG(trans)) + + return output + + +class Plot: + """Acts like Fig, but draws a coordinate axis. You also need to supply plot ranges. + + Plot(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...) + + xmin, xmax required minimum and maximum x values (in the objs' coordinates) + ymin, ymax required minimum and maximum y values (in the objs' coordinates) + obj optional list drawing primitives + keyword options keyword list options defined below + + The following are keyword options, with their default values: + + trans None transformation function + x, y 5, 5 upper-left corner of the Plot in SVG coordinates + width, height 90, 90 width and height of the Plot in SVG coordinates + flipx, flipy False, True flip the sign of the coordinate axis + minusInfinity -1000 if an axis is logarithmic and an object is plotted at 0 or + a negative value, -1000 will be used as a stand-in for NaN + atx, aty 0, 0 the place where the coordinate axes cross + xticks -10 request ticks according to the standard tick specification + (see help(Ticks)) + xminiticks True request miniticks according to the standard minitick + specification + xlabels True request tick labels according to the standard tick label + specification + xlogbase None if a number, the axis and transformation are logarithmic + with ticks at the given base (10 being the most common) + (same for y) + arrows None if a new identifier, create arrow markers and draw them + at the ends of the coordinate axes + text_attr {} a dictionary of attributes for label text + axis_attr {} a dictionary of attributes for the axis lines + """ + + def __repr__(self): + if self.trans is None: + return "" % len(self.d) + else: + return "" % (len(self.d), self.trans.func_name) + + def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds): + self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax + self.d = list(d) + defaults = {"trans": None, + "x": 5, "y": 5, "width": 90, "height": 90, + "flipx": False, "flipy": True, + "minusInfinity": -1000, + "atx": 0, "xticks": -10, "xminiticks": True, "xlabels": True, "xlogbase": None, + "aty": 0, "yticks": -10, "yminiticks": True, "ylabels": True, "ylogbase": None, + "arrows": None, + "text_attr": {}, "axis_attr": {}, + } + defaults.update(kwds) + kwds = defaults + + self.trans = kwds["trans"]; del kwds["trans"] + self.x = kwds["x"]; del kwds["x"] + self.y = kwds["y"]; del kwds["y"] + self.width = kwds["width"]; del kwds["width"] + self.height = kwds["height"]; del kwds["height"] + self.flipx = kwds["flipx"]; del kwds["flipx"] + self.flipy = kwds["flipy"]; del kwds["flipy"] + self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"] + self.atx = kwds["atx"]; del kwds["atx"] + self.xticks = kwds["xticks"]; del kwds["xticks"] + self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"] + self.xlabels = kwds["xlabels"]; del kwds["xlabels"] + self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"] + self.aty = kwds["aty"]; del kwds["aty"] + self.yticks = kwds["yticks"]; del kwds["yticks"] + self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"] + self.ylabels = kwds["ylabels"]; del kwds["ylabels"] + self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"] + self.arrows = kwds["arrows"]; del kwds["arrows"] + self.text_attr = kwds["text_attr"]; del kwds["text_attr"] + self.axis_attr = kwds["axis_attr"]; del kwds["axis_attr"] + if len(kwds) != 0: + raise TypeError, "Plot() got unexpected keyword arguments %s" % kwds.keys() + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + if trans is None: + trans = self.trans + if isinstance(trans, basestring): + trans = totrans(trans) + + self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax, + x=self.x, y=self.y, width=self.width, height=self.height, + xlogbase=self.xlogbase, ylogbase=self.ylogbase, + minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy) + + d = ([Axes(self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty, + self.xticks, self.xminiticks, self.xlabels, self.xlogbase, + self.yticks, self.yminiticks, self.ylabels, self.ylogbase, + self.arrows, self.text_attr, **self.axis_attr)] + + self.d) + + return Fig(Fig(*d, **{"trans": trans})).SVG(self.last_window) + + +class Frame: + text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, } + axis_defaults = {} + + tick_length = 1.5 + minitick_length = 0.75 + text_xaxis_offset = 1. + text_yaxis_offset = 2. + text_xtitle_offset = 6. + text_ytitle_offset = 12. + + def __repr__(self): + return "" % len(self.d) + + def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds): + """Acts like Fig, but draws a coordinate frame around the data. You also need to supply plot ranges. + + Frame(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...) + + xmin, xmax required minimum and maximum x values (in the objs' coordinates) + ymin, ymax required minimum and maximum y values (in the objs' coordinates) + obj optional list drawing primitives + keyword options keyword list options defined below + + The following are keyword options, with their default values: + + x, y 20, 5 upper-left corner of the Frame in SVG coordinates + width, height 75, 80 width and height of the Frame in SVG coordinates + flipx, flipy False, True flip the sign of the coordinate axis + minusInfinity -1000 if an axis is logarithmic and an object is plotted at 0 or + a negative value, -1000 will be used as a stand-in for NaN + xtitle None if a string, label the x axis + xticks -10 request ticks according to the standard tick specification + (see help(Ticks)) + xminiticks True request miniticks according to the standard minitick + specification + xlabels True request tick labels according to the standard tick label + specification + xlogbase None if a number, the axis and transformation are logarithmic + with ticks at the given base (10 being the most common) + (same for y) + text_attr {} a dictionary of attributes for label text + axis_attr {} a dictionary of attributes for the axis lines + """ + + self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax + self.d = list(d) + defaults = {"x": 20, "y": 5, "width": 75, "height": 80, + "flipx": False, "flipy": True, "minusInfinity": -1000, + "xtitle": None, "xticks": -10, "xminiticks": True, "xlabels": True, + "x2labels": None, "xlogbase": None, + "ytitle": None, "yticks": -10, "yminiticks": True, "ylabels": True, + "y2labels": None, "ylogbase": None, + "text_attr": {}, "axis_attr": {}, + } + defaults.update(kwds) + kwds = defaults + + self.x = kwds["x"]; del kwds["x"] + self.y = kwds["y"]; del kwds["y"] + self.width = kwds["width"]; del kwds["width"] + self.height = kwds["height"]; del kwds["height"] + self.flipx = kwds["flipx"]; del kwds["flipx"] + self.flipy = kwds["flipy"]; del kwds["flipy"] + self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"] + self.xtitle = kwds["xtitle"]; del kwds["xtitle"] + self.xticks = kwds["xticks"]; del kwds["xticks"] + self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"] + self.xlabels = kwds["xlabels"]; del kwds["xlabels"] + self.x2labels = kwds["x2labels"]; del kwds["x2labels"] + self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"] + self.ytitle = kwds["ytitle"]; del kwds["ytitle"] + self.yticks = kwds["yticks"]; del kwds["yticks"] + self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"] + self.ylabels = kwds["ylabels"]; del kwds["ylabels"] + self.y2labels = kwds["y2labels"]; del kwds["y2labels"] + self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"] + + self.text_attr = dict(self.text_defaults) + self.text_attr.update(kwds["text_attr"]); del kwds["text_attr"] + + self.axis_attr = dict(self.axis_defaults) + self.axis_attr.update(kwds["axis_attr"]); del kwds["axis_attr"] + + if len(kwds) != 0: + raise TypeError, "Frame() got unexpected keyword arguments %s" % kwds.keys() + + def SVG(self): + """Apply the window transformation and return an SVG object.""" + + self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax, + x=self.x, y=self.y, width=self.width, height=self.height, + xlogbase=self.xlogbase, ylogbase=self.ylogbase, + minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy) + + left = YAxis(self.ymin, self.ymax, self.xmin, self.yticks, self.yminiticks, self.ylabels, self.ylogbase, + None, None, None, self.text_attr, **self.axis_attr) + right = YAxis(self.ymin, self.ymax, self.xmax, self.yticks, self.yminiticks, self.y2labels, self.ylogbase, + None, None, None, self.text_attr, **self.axis_attr) + bottom = XAxis(self.xmin, self.xmax, self.ymin, self.xticks, self.xminiticks, self.xlabels, self.xlogbase, + None, None, None, self.text_attr, **self.axis_attr) + top = XAxis(self.xmin, self.xmax, self.ymax, self.xticks, self.xminiticks, self.x2labels, self.xlogbase, + None, None, None, self.text_attr, **self.axis_attr) + + left.tick_start = -self.tick_length + left.tick_end = 0 + left.minitick_start = -self.minitick_length + left.minitick_end = 0. + left.text_start = self.text_yaxis_offset + + right.tick_start = 0. + right.tick_end = self.tick_length + right.minitick_start = 0. + right.minitick_end = self.minitick_length + right.text_start = -self.text_yaxis_offset + right.text_attr["text-anchor"] = "start" + + bottom.tick_start = 0. + bottom.tick_end = self.tick_length + bottom.minitick_start = 0. + bottom.minitick_end = self.minitick_length + bottom.text_start = -self.text_xaxis_offset + + top.tick_start = -self.tick_length + top.tick_end = 0. + top.minitick_start = -self.minitick_length + top.minitick_end = 0. + top.text_start = self.text_xaxis_offset + top.text_attr["dominant-baseline"] = "text-after-edge" + + output = Fig(*self.d).SVG(self.last_window) + output.prepend(left.SVG(self.last_window)) + output.prepend(bottom.SVG(self.last_window)) + output.prepend(right.SVG(self.last_window)) + output.prepend(top.SVG(self.last_window)) + + if self.xtitle is not None: + 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)) + if self.ytitle is not None: + 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)) + return output + +###################################################################### + +def pathtoPath(svg): + """Converts SVG("path", d="...") into Path(d=[...]).""" + if not isinstance(svg, SVG) or svg.t != "path": + raise TypeError, "Only SVG objects can be converted into Paths" + attr = dict(svg.attr) + d = attr["d"] + del attr["d"] + for key in attr.keys(): + if not isinstance(key, str): + value = attr[key] + del attr[key] + attr[str(key)] = value + return Path(d, **attr) + + +class Path: + """Path represents an SVG path, an arbitrary set of curves and + straight segments. Unlike SVG("path", d="..."), Path stores + coordinates as a list of numbers, rather than a string, so that it is + transformable in a Fig. + + Path(d, attribute=value) + + d required path data + attribute=value pairs keyword list SVG attributes + + See http://www.w3.org/TR/SVG/paths.html for specification of paths + from text. + + Internally, Path data is a list of tuples with these definitions: + + * ("Z/z",): close the current path + * ("H/h", x) or ("V/v", y): a horizontal or vertical line + segment to x or y + * ("M/m/L/l/T/t", x, y, global): moveto, lineto, or smooth + quadratic curveto point (x, y). If global=True, (x, y) should + not be transformed. + * ("S/sQ/q", cx, cy, cglobal, x, y, global): polybezier or + smooth quadratic curveto point (x, y) using (cx, cy) as a + control point. If cglobal or global=True, (cx, cy) or (x, y) + should not be transformed. + * ("C/c", c1x, c1y, c1global, c2x, c2y, c2global, x, y, global): + cubic curveto point (x, y) using (c1x, c1y) and (c2x, c2y) as + control points. If c1global, c2global, or global=True, (c1x, c1y), + (c2x, c2y), or (x, y) should not be transformed. + * ("A/a", rx, ry, rglobal, x-axis-rotation, angle, large-arc-flag, + sweep-flag, x, y, global): arcto point (x, y) using the + aforementioned parameters. + * (",/.", rx, ry, rglobal, angle, x, y, global): an ellipse at + point (x, y) with radii (rx, ry). If angle is 0, the whole + ellipse is drawn; otherwise, a partial ellipse is drawn. + """ + defaults = {} + + def __repr__(self): + return "" % (len(self.d), self.attr) + + def __init__(self, d=[], **attr): + if isinstance(d, basestring): + self.d = self.parse(d) + else: + self.d = list(d) + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def parse_whitespace(self, index, pathdata): + """Part of Path's text-command parsing algorithm; used internally.""" + while index < len(pathdata) and pathdata[index] in (" ", "\t", "\r", "\n", ","): + index += 1 + return index, pathdata + + def parse_command(self, index, pathdata): + """Part of Path's text-command parsing algorithm; used internally.""" + index, pathdata = self.parse_whitespace(index, pathdata) + + if index >= len(pathdata): + return None, index, pathdata + command = pathdata[index] + if "A" <= command <= "Z" or "a" <= command <= "z": + index += 1 + return command, index, pathdata + else: + return None, index, pathdata + + def parse_number(self, index, pathdata): + """Part of Path's text-command parsing algorithm; used internally.""" + index, pathdata = self.parse_whitespace(index, pathdata) + + if index >= len(pathdata): + return None, index, pathdata + first_digit = pathdata[index] + + if "0" <= first_digit <= "9" or first_digit in ("-", "+", "."): + start = index + while index < len(pathdata) and ("0" <= pathdata[index] <= "9" or pathdata[index] in ("-", "+", ".", "e", "E")): + index += 1 + end = index + + index = end + return float(pathdata[start:end]), index, pathdata + else: + return None, index, pathdata + + def parse_boolean(self, index, pathdata): + """Part of Path's text-command parsing algorithm; used internally.""" + index, pathdata = self.parse_whitespace(index, pathdata) + + if index >= len(pathdata): + return None, index, pathdata + first_digit = pathdata[index] + + if first_digit in ("0", "1"): + index += 1 + return int(first_digit), index, pathdata + else: + return None, index, pathdata + + def parse(self, pathdata): + """Parses text-commands, converting them into a list of tuples. + Called by the constructor.""" + output = [] + index = 0 + while True: + command, index, pathdata = self.parse_command(index, pathdata) + index, pathdata = self.parse_whitespace(index, pathdata) + + if command is None and index == len(pathdata): + break # this is the normal way out of the loop + if command in ("Z", "z"): + output.append((command,)) + + ###################### + elif command in ("H", "h", "V", "v"): + errstring = "Path command \"%s\" requires a number at index %d" % (command, index) + num1, index, pathdata = self.parse_number(index, pathdata) + if num1 is None: + raise ValueError, errstring + + while num1 is not None: + output.append((command, num1)) + num1, index, pathdata = self.parse_number(index, pathdata) + + ###################### + elif command in ("M", "m", "L", "l", "T", "t"): + errstring = "Path command \"%s\" requires an x,y pair at index %d" % (command, index) + num1, index, pathdata = self.parse_number(index, pathdata) + num2, index, pathdata = self.parse_number(index, pathdata) + + if num1 is None: + raise ValueError, errstring + + while num1 is not None: + if num2 is None: + raise ValueError, errstring + output.append((command, num1, num2, False)) + + num1, index, pathdata = self.parse_number(index, pathdata) + num2, index, pathdata = self.parse_number(index, pathdata) + + ###################### + elif command in ("S", "s", "Q", "q"): + errstring = "Path command \"%s\" requires a cx,cy,x,y quadruplet at index %d" % (command, index) + num1, index, pathdata = self.parse_number(index, pathdata) + num2, index, pathdata = self.parse_number(index, pathdata) + num3, index, pathdata = self.parse_number(index, pathdata) + num4, index, pathdata = self.parse_number(index, pathdata) + + if num1 is None: + raise ValueError, errstring + + while num1 is not None: + if num2 is None or num3 is None or num4 is None: + raise ValueError, errstring + output.append((command, num1, num2, False, num3, num4, False)) + + num1, index, pathdata = self.parse_number(index, pathdata) + num2, index, pathdata = self.parse_number(index, pathdata) + num3, index, pathdata = self.parse_number(index, pathdata) + num4, index, pathdata = self.parse_number(index, pathdata) + + ###################### + elif command in ("C", "c"): + errstring = "Path command \"%s\" requires a c1x,c1y,c2x,c2y,x,y sextuplet at index %d" % (command, index) + num1, index, pathdata = self.parse_number(index, pathdata) + num2, index, pathdata = self.parse_number(index, pathdata) + num3, index, pathdata = self.parse_number(index, pathdata) + num4, index, pathdata = self.parse_number(index, pathdata) + num5, index, pathdata = self.parse_number(index, pathdata) + num6, index, pathdata = self.parse_number(index, pathdata) + + if num1 is None: + raise ValueError, errstring + + while num1 is not None: + if num2 is None or num3 is None or num4 is None or num5 is None or num6 is None: + raise ValueError, errstring + + output.append((command, num1, num2, False, num3, num4, False, num5, num6, False)) + + num1, index, pathdata = self.parse_number(index, pathdata) + num2, index, pathdata = self.parse_number(index, pathdata) + num3, index, pathdata = self.parse_number(index, pathdata) + num4, index, pathdata = self.parse_number(index, pathdata) + num5, index, pathdata = self.parse_number(index, pathdata) + num6, index, pathdata = self.parse_number(index, pathdata) + + ###################### + elif command in ("A", "a"): + errstring = "Path command \"%s\" requires a rx,ry,angle,large-arc-flag,sweep-flag,x,y septuplet at index %d" % (command, index) + num1, index, pathdata = self.parse_number(index, pathdata) + num2, index, pathdata = self.parse_number(index, pathdata) + num3, index, pathdata = self.parse_number(index, pathdata) + num4, index, pathdata = self.parse_boolean(index, pathdata) + num5, index, pathdata = self.parse_boolean(index, pathdata) + num6, index, pathdata = self.parse_number(index, pathdata) + num7, index, pathdata = self.parse_number(index, pathdata) + + if num1 is None: + raise ValueError, errstring + + while num1 is not None: + if num2 is None or num3 is None or num4 is None or num5 is None or num6 is None or num7 is None: + raise ValueError, errstring + + output.append((command, num1, num2, False, num3, num4, num5, num6, num7, False)) + + num1, index, pathdata = self.parse_number(index, pathdata) + num2, index, pathdata = self.parse_number(index, pathdata) + num3, index, pathdata = self.parse_number(index, pathdata) + num4, index, pathdata = self.parse_boolean(index, pathdata) + num5, index, pathdata = self.parse_boolean(index, pathdata) + num6, index, pathdata = self.parse_number(index, pathdata) + num7, index, pathdata = self.parse_number(index, pathdata) + + return output + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + if isinstance(trans, basestring): + trans = totrans(trans) + + x, y, X, Y = None, None, None, None + output = [] + for datum in self.d: + if not isinstance(datum, (tuple, list)): + raise TypeError, "pathdata elements must be tuples/lists" + + command = datum[0] + + ###################### + if command in ("Z", "z"): + x, y, X, Y = None, None, None, None + output.append("Z") + + ###################### + elif command in ("H", "h", "V", "v"): + command, num1 = datum + + if command == "H" or (command == "h" and x is None): + x = num1 + elif command == "h": + x += num1 + elif command == "V" or (command == "v" and y is None): + y = num1 + elif command == "v": + y += num1 + + if trans is None: + X, Y = x, y + else: + X, Y = trans(x, y) + + output.append("L%g %g" % (X, Y)) + + ###################### + elif command in ("M", "m", "L", "l", "T", "t"): + command, num1, num2, isglobal12 = datum + + if trans is None or isglobal12: + if command.isupper() or X is None or Y is None: + X, Y = num1, num2 + else: + X += num1 + Y += num2 + x, y = X, Y + + else: + if command.isupper() or x is None or y is None: + x, y = num1, num2 + else: + x += num1 + y += num2 + X, Y = trans(x, y) + + COMMAND = command.capitalize() + output.append("%s%g %g" % (COMMAND, X, Y)) + + ###################### + elif command in ("S", "s", "Q", "q"): + command, num1, num2, isglobal12, num3, num4, isglobal34 = datum + + if trans is None or isglobal12: + if command.isupper() or X is None or Y is None: + CX, CY = num1, num2 + else: + CX = X + num1 + CY = Y + num2 + + else: + if command.isupper() or x is None or y is None: + cx, cy = num1, num2 + else: + cx = x + num1 + cy = y + num2 + CX, CY = trans(cx, cy) + + if trans is None or isglobal34: + if command.isupper() or X is None or Y is None: + X, Y = num3, num4 + else: + X += num3 + Y += num4 + x, y = X, Y + + else: + if command.isupper() or x is None or y is None: + x, y = num3, num4 + else: + x += num3 + y += num4 + X, Y = trans(x, y) + + COMMAND = command.capitalize() + output.append("%s%g %g %g %g" % (COMMAND, CX, CY, X, Y)) + + ###################### + elif command in ("C", "c"): + command, num1, num2, isglobal12, num3, num4, isglobal34, num5, num6, isglobal56 = datum + + if trans is None or isglobal12: + if command.isupper() or X is None or Y is None: + C1X, C1Y = num1, num2 + else: + C1X = X + num1 + C1Y = Y + num2 + + else: + if command.isupper() or x is None or y is None: + c1x, c1y = num1, num2 + else: + c1x = x + num1 + c1y = y + num2 + C1X, C1Y = trans(c1x, c1y) + + if trans is None or isglobal34: + if command.isupper() or X is None or Y is None: + C2X, C2Y = num3, num4 + else: + C2X = X + num3 + C2Y = Y + num4 + + else: + if command.isupper() or x is None or y is None: + c2x, c2y = num3, num4 + else: + c2x = x + num3 + c2y = y + num4 + C2X, C2Y = trans(c2x, c2y) + + if trans is None or isglobal56: + if command.isupper() or X is None or Y is None: + X, Y = num5, num6 + else: + X += num5 + Y += num6 + x, y = X, Y + + else: + if command.isupper() or x is None or y is None: + x, y = num5, num6 + else: + x += num5 + y += num6 + X, Y = trans(x, y) + + COMMAND = command.capitalize() + output.append("%s%g %g %g %g %g %g" % (COMMAND, C1X, C1Y, C2X, C2Y, X, Y)) + + ###################### + elif command in ("A", "a"): + command, num1, num2, isglobal12, angle, large_arc_flag, sweep_flag, num3, num4, isglobal34 = datum + + oldx, oldy = x, y + OLDX, OLDY = X, Y + + if trans is None or isglobal34: + if command.isupper() or X is None or Y is None: + X, Y = num3, num4 + else: + X += num3 + Y += num4 + x, y = X, Y + + else: + if command.isupper() or x is None or y is None: + x, y = num3, num4 + else: + x += num3 + y += num4 + X, Y = trans(x, y) + + if x is not None and y is not None: + centerx, centery = (x + oldx)/2., (y + oldy)/2. + CENTERX, CENTERY = (X + OLDX)/2., (Y + OLDY)/2. + + if trans is None or isglobal12: + RX = CENTERX + num1 + RY = CENTERY + num2 + + else: + rx = centerx + num1 + ry = centery + num2 + RX, RY = trans(rx, ry) + + COMMAND = command.capitalize() + output.append("%s%g %g %g %d %d %g %g" % (COMMAND, RX - CENTERX, RY - CENTERY, angle, large_arc_flag, sweep_flag, X, Y)) + + elif command in (",", "."): + command, num1, num2, isglobal12, angle, num3, num4, isglobal34 = datum + if trans is None or isglobal34: + if command == "." or X is None or Y is None: + X, Y = num3, num4 + else: + X += num3 + Y += num4 + x, y = None, None + + else: + if command == "." or x is None or y is None: + x, y = num3, num4 + else: + x += num3 + y += num4 + X, Y = trans(x, y) + + if trans is None or isglobal12: + RX = X + num1 + RY = Y + num2 + + else: + rx = x + num1 + ry = y + num2 + RX, RY = trans(rx, ry) + + RX, RY = RX - X, RY - Y + + X1, Y1 = X + RX * math.cos(angle*math.pi/180.), Y + RX * math.sin(angle*math.pi/180.) + X2, Y2 = X + RY * math.sin(angle*math.pi/180.), Y - RY * math.cos(angle*math.pi/180.) + X3, Y3 = X - RX * math.cos(angle*math.pi/180.), Y - RX * math.sin(angle*math.pi/180.) + X4, Y4 = X - RY * math.sin(angle*math.pi/180.), Y + RY * math.cos(angle*math.pi/180.) + + 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" % ( + X1, Y1, RX, RY, angle, X2, Y2, RX, RY, angle, X3, Y3, RX, RY, angle, X4, Y4, RX, RY, angle, X1, Y1)) + + return SVG("path", d="".join(output), **self.attr) + +###################################################################### + +def funcRtoC(expr, var="t", globals=None, locals=None): + """Converts a complex "z(t)" string to a function acceptable for Curve. + + expr required string in the form "z(t)" + var default="t" name of the independent variable + globals default=None dict of global variables used in the expression; + you may want to use Python's builtin globals() + locals default=None dict of local variables + """ + if locals is None: + locals = {} # python 2.3's eval() won't accept None + g = cmath.__dict__ + if globals is not None: + g.update(globals) + output = eval("lambda %s: (%s)" % (var, expr), g, locals) + split = lambda z: (z.real, z.imag) + output2 = lambda t: split(output(t)) + set_func_name(output2, "%s -> %s" % (var, expr)) + return output2 + + +def funcRtoR2(expr, var="t", globals=None, locals=None): + """Converts a "f(t), g(t)" string to a function acceptable for Curve. + + expr required string in the form "f(t), g(t)" + var default="t" name of the independent variable + globals default=None dict of global variables used in the expression; + you may want to use Python's builtin globals() + locals default=None dict of local variables + """ + if locals is None: + locals = {} # python 2.3's eval() won't accept None + g = math.__dict__ + if globals is not None: + g.update(globals) + output = eval("lambda %s: (%s)" % (var, expr), g, locals) + set_func_name(output, "%s -> %s" % (var, expr)) + return output + + +def funcRtoR(expr, var="x", globals=None, locals=None): + """Converts a "f(x)" string to a function acceptable for Curve. + + expr required string in the form "f(x)" + var default="x" name of the independent variable + globals default=None dict of global variables used in the expression; + you may want to use Python's builtin globals() + locals default=None dict of local variables + """ + if locals is None: + locals = {} # python 2.3's eval() won't accept None + g = math.__dict__ + if globals is not None: + g.update(globals) + output = eval("lambda %s: (%s, %s)" % (var, var, expr), g, locals) + set_func_name(output, "%s -> %s" % (var, expr)) + return output + + +class Curve: + """Draws a parametric function as a path. + + Curve(f, low, high, loop, attribute=value) + + f required a Python callable or string in + the form "f(t), g(t)" + low, high required left and right endpoints + loop default=False if True, connect the endpoints + attribute=value pairs keyword list SVG attributes + """ + defaults = {} + random_sampling = True + recursion_limit = 15 + linearity_limit = 0.05 + discontinuity_limit = 5. + + def __repr__(self): + return "" % (self.f, self.low, self.high, self.attr) + + def __init__(self, f, low, high, loop=False, **attr): + self.f = f + self.low = low + self.high = high + self.loop = loop + + self.attr = dict(self.defaults) + self.attr.update(attr) + + ### nested class Sample + class Sample: + def __repr__(self): + t, x, y, X, Y = self.t, self.x, self.y, self.X, self.Y + if t is not None: + t = "%g" % t + if x is not None: + x = "%g" % x + if y is not None: + y = "%g" % y + if X is not None: + X = "%g" % X + if Y is not None: + Y = "%g" % Y + return "" % (t, x, y, X, Y) + + def __init__(self, t): + self.t = t + + def link(self, left, right): + self.left, self.right = left, right + + def evaluate(self, f, trans): + self.x, self.y = f(self.t) + if trans is None: + self.X, self.Y = self.x, self.y + else: + self.X, self.Y = trans(self.x, self.y) + ### end Sample + + ### nested class Samples + class Samples: + def __repr__(self): + return "" % len(self) + + def __init__(self, left, right): + self.left, self.right = left, right + + def __len__(self): + count = 0 + current = self.left + while current is not None: + count += 1 + current = current.right + return count + + def __iter__(self): + self.current = self.left + return self + + def next(self): + current = self.current + if current is None: + raise StopIteration + self.current = self.current.right + return current + ### end nested class + + def sample(self, trans=None): + """Adaptive-sampling algorithm that chooses the best sample points + for a parametric curve between two endpoints and detects + discontinuities. Called by SVG().""" + oldrecursionlimit = sys.getrecursionlimit() + sys.setrecursionlimit(self.recursion_limit + 100) + try: + # the best way to keep all the information while sampling is to make a linked list + if not (self.low < self.high): + raise ValueError, "low must be less than high" + low, high = self.Sample(float(self.low)), self.Sample(float(self.high)) + low.link(None, high) + high.link(low, None) + + low.evaluate(self.f, trans) + high.evaluate(self.f, trans) + + # adaptive sampling between the low and high points + self.subsample(low, high, 0, trans) + + # Prune excess points where the curve is nearly linear + left = low + while left.right is not None: + # increment mid and right + mid = left.right + right = mid.right + if (right is not None and + left.X is not None and left.Y is not None and + mid.X is not None and mid.Y is not None and + right.X is not None and right.Y is not None): + numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y) + denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2) + if denom != 0. and abs(numer/denom) < self.linearity_limit: + # drop mid (the garbage collector will get it) + left.right = right + right.left = left + else: + # increment left + left = left.right + else: + left = left.right + + self.last_samples = self.Samples(low, high) + + finally: + sys.setrecursionlimit(oldrecursionlimit) + + def subsample(self, left, right, depth, trans=None): + """Part of the adaptive-sampling algorithm that chooses the best + sample points. Called by sample().""" + + if self.random_sampling: + mid = self.Sample(left.t + random.uniform(0.3, 0.7) * (right.t - left.t)) + else: + mid = self.Sample(left.t + 0.5 * (right.t - left.t)) + + left.right = mid + right.left = mid + mid.link(left, right) + mid.evaluate(self.f, trans) + + # calculate the distance of closest approach of mid to the line between left and right + numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y) + denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2) + + # if we haven't sampled enough or left fails to be close enough to right, or mid fails to be linear enough... + if (depth < 3 or + (denom == 0 and left.t != right.t) or + denom > self.discontinuity_limit or + (denom != 0. and abs(numer/denom) > self.linearity_limit)): + + # and we haven't sampled too many points + if depth < self.recursion_limit: + self.subsample(left, mid, depth+1, trans) + self.subsample(mid, right, depth+1, trans) + + else: + # We've sampled many points and yet it's still not a small linear gap. + # Break the line: it's a discontinuity + mid.y = mid.Y = None + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + return self.Path(trans).SVG() + + def Path(self, trans=None, local=False): + """Apply the transformation "trans" and return a Path object in + global coordinates. If local=True, return a Path in local coordinates + (which must be transformed again).""" + + if isinstance(trans, basestring): + trans = totrans(trans) + if isinstance(self.f, basestring): + self.f = funcRtoR2(self.f) + + self.sample(trans) + + output = [] + for s in self.last_samples: + if s.X is not None and s.Y is not None: + if s.left is None or s.left.Y is None: + command = "M" + else: + command = "L" + + if local: + output.append((command, s.x, s.y, False)) + else: + output.append((command, s.X, s.Y, True)) + + if self.loop: + output.append(("Z",)) + return Path(output, **self.attr) + +###################################################################### + +class Poly: + """Draws a curve specified by a sequence of points. The curve may be + piecewise linear, like a polygon, or a Bezier curve. + + Poly(d, mode, loop, attribute=value) + + d required list of tuples representing points + and possibly control points + mode default="L" "lines", "bezier", "velocity", + "foreback", "smooth", or an abbreviation + loop default=False if True, connect the first and last + point, closing the loop + attribute=value pairs keyword list SVG attributes + + The format of the tuples in d depends on the mode. + + "lines"/"L" d=[(x,y), (x,y), ...] + piecewise-linear segments joining the (x,y) points + "bezier"/"B" d=[(x, y, c1x, c1y, c2x, c2y), ...] + Bezier curve with two control points (control points + preceed (x,y), as in SVG paths). If (c1x,c1y) and + (c2x,c2y) both equal (x,y), you get a linear + interpolation ("lines") + "velocity"/"V" d=[(x, y, vx, vy), ...] + curve that passes through (x,y) with velocity (vx,vy) + (one unit of arclength per unit time); in other words, + (vx,vy) is the tangent vector at (x,y). If (vx,vy) is + (0,0), you get a linear interpolation ("lines"). + "foreback"/"F" d=[(x, y, bx, by, fx, fy), ...] + like "velocity" except that there is a left derivative + (bx,by) and a right derivative (fx,fy). If (bx,by) + equals (fx,fy) (with no minus sign), you get a + "velocity" curve + "smooth"/"S" d=[(x,y), (x,y), ...] + a "velocity" interpolation with (vx,vy)[i] equal to + ((x,y)[i+1] - (x,y)[i-1])/2: the minimal derivative + """ + defaults = {} + + def __repr__(self): + return "" % ( + len(self.d), self.mode, repr(self.loop), self.attr) + + def __init__(self, d=[], mode="L", loop=False, **attr): + self.d = list(d) + self.mode = mode + self.loop = loop + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + return self.Path(trans).SVG() + + def Path(self, trans=None, local=False): + """Apply the transformation "trans" and return a Path object in + global coordinates. If local=True, return a Path in local coordinates + (which must be transformed again).""" + if isinstance(trans, basestring): + trans = totrans(trans) + + if self.mode[0] == "L" or self.mode[0] == "l": + mode = "L" + elif self.mode[0] == "B" or self.mode[0] == "b": + mode = "B" + elif self.mode[0] == "V" or self.mode[0] == "v": + mode = "V" + elif self.mode[0] == "F" or self.mode[0] == "f": + mode = "F" + elif self.mode[0] == "S" or self.mode[0] == "s": + mode = "S" + + vx, vy = [0.]*len(self.d), [0.]*len(self.d) + for i in xrange(len(self.d)): + inext = (i+1) % len(self.d) + iprev = (i-1) % len(self.d) + + vx[i] = (self.d[inext][0] - self.d[iprev][0])/2. + vy[i] = (self.d[inext][1] - self.d[iprev][1])/2. + if not self.loop and (i == 0 or i == len(self.d)-1): + vx[i], vy[i] = 0., 0. + + else: + raise ValueError, "mode must be \"lines\", \"bezier\", \"velocity\", \"foreback\", \"smooth\", or an abbreviation" + + d = [] + indexes = range(len(self.d)) + if self.loop and len(self.d) > 0: + indexes.append(0) + + for i in indexes: + inext = (i+1) % len(self.d) + iprev = (i-1) % len(self.d) + + x, y = self.d[i][0], self.d[i][1] + + if trans is None: + X, Y = x, y + else: + X, Y = trans(x, y) + + if d == []: + if local: + d.append(("M", x, y, False)) + else: + d.append(("M", X, Y, True)) + + elif mode == "L": + if local: + d.append(("L", x, y, False)) + else: + d.append(("L", X, Y, True)) + + elif mode == "B": + c1x, c1y = self.d[i][2], self.d[i][3] + if trans is None: + C1X, C1Y = c1x, c1y + else: + C1X, C1Y = trans(c1x, c1y) + + c2x, c2y = self.d[i][4], self.d[i][5] + if trans is None: + C2X, C2Y = c2x, c2y + else: + C2X, C2Y = trans(c2x, c2y) + + if local: + d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False)) + else: + d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True)) + + elif mode == "V": + c1x, c1y = self.d[iprev][2]/3. + self.d[iprev][0], self.d[iprev][3]/3. + self.d[iprev][1] + c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y + + if trans is None: + C1X, C1Y = c1x, c1y + else: + C1X, C1Y = trans(c1x, c1y) + if trans is None: + C2X, C2Y = c2x, c2y + else: + C2X, C2Y = trans(c2x, c2y) + + if local: + d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False)) + else: + d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True)) + + elif mode == "F": + c1x, c1y = self.d[iprev][4]/3. + self.d[iprev][0], self.d[iprev][5]/3. + self.d[iprev][1] + c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y + + if trans is None: + C1X, C1Y = c1x, c1y + else: + C1X, C1Y = trans(c1x, c1y) + if trans is None: + C2X, C2Y = c2x, c2y + else: + C2X, C2Y = trans(c2x, c2y) + + if local: + d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False)) + else: + d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True)) + + elif mode == "S": + c1x, c1y = vx[iprev]/3. + self.d[iprev][0], vy[iprev]/3. + self.d[iprev][1] + c2x, c2y = vx[i]/-3. + x, vy[i]/-3. + y + + if trans is None: + C1X, C1Y = c1x, c1y + else: + C1X, C1Y = trans(c1x, c1y) + if trans is None: + C2X, C2Y = c2x, c2y + else: + C2X, C2Y = trans(c2x, c2y) + + if local: + d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False)) + else: + d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True)) + + if self.loop and len(self.d) > 0: + d.append(("Z",)) + + return Path(d, **self.attr) + +###################################################################### + +class Text: + """Draws a text string at a specified point in local coordinates. + + x, y required location of the point in local coordinates + d required text/Unicode string + attribute=value pairs keyword list SVG attributes + """ + + defaults = {"stroke": "none", "fill": "black", "font-size": 5, } + + def __repr__(self): + return "" % (repr(self.d), self.x, self.y, self.attr) + + def __init__(self, x, y, d, **attr): + self.x = x + self.y = y + self.d = unicode(d) + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + if isinstance(trans, basestring): + trans = totrans(trans) + + X, Y = self.x, self.y + if trans is not None: + X, Y = trans(X, Y) + return SVG("text", self.d, x=X, y=Y, **self.attr) + + +class TextGlobal: + """Draws a text string at a specified point in global coordinates. + + x, y required location of the point in global coordinates + d required text/Unicode string + attribute=value pairs keyword list SVG attributes + """ + defaults = {"stroke": "none", "fill": "black", "font-size": 5, } + + def __repr__(self): + return "" % (repr(self.d), str(self.x), str(self.y), self.attr) + + def __init__(self, x, y, d, **attr): + self.x = x + self.y = y + self.d = unicode(d) + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + return SVG("text", self.d, x=self.x, y=self.y, **self.attr) + +###################################################################### + +_symbol_templates = {"dot": SVG("symbol", SVG("circle", cx=0, cy=0, r=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), + "box": SVG("symbol", SVG("rect", x1=-1, y1=-1, x2=1, y2=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), + "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"), + "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"), + } + +def make_symbol(id, shape="dot", **attr): + """Creates a new instance of an SVG symbol to avoid cross-linking objects. + + id required a new identifier (string/Unicode) + shape default="dot" the shape name from _symbol_templates + attribute=value list keyword list modify the SVG attributes of the new symbol + """ + output = copy.deepcopy(_symbol_templates[shape]) + for i in output.sub: + i.attr.update(attr_preprocess(attr)) + output["id"] = id + return output + +_circular_dot = make_symbol("circular_dot") + + +class Dots: + """Dots draws SVG symbols at a set of points. + + d required list of (x,y) points + symbol default=None SVG symbol or a new identifier to + label an auto-generated symbol; + if None, use pre-defined _circular_dot + width, height default=1, 1 width and height of the symbols + in SVG coordinates + attribute=value pairs keyword list SVG attributes + """ + defaults = {} + + def __repr__(self): + return "" % (len(self.d), self.attr) + + def __init__(self, d=[], symbol=None, width=1., height=1., **attr): + self.d = list(d) + self.width = width + self.height = height + + self.attr = dict(self.defaults) + self.attr.update(attr) + + if symbol is None: + self.symbol = _circular_dot + elif isinstance(symbol, SVG): + self.symbol = symbol + else: + self.symbol = make_symbol(symbol) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + if isinstance(trans, basestring): + trans = totrans(trans) + + output = SVG("g", SVG("defs", self.symbol)) + id = "#%s" % self.symbol["id"] + + for p in self.d: + x, y = p[0], p[1] + + if trans is None: + X, Y = x, y + else: + X, Y = trans(x, y) + + item = SVG("use", x=X, y=Y, xlink__href=id) + if self.width is not None: + item["width"] = self.width + if self.height is not None: + item["height"] = self.height + output.append(item) + + return output + +###################################################################### + +_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"), + "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"), + } + +def make_marker(id, shape, **attr): + """Creates a new instance of an SVG marker to avoid cross-linking objects. + + id required a new identifier (string/Unicode) + shape required the shape name from _marker_templates + attribute=value list keyword list modify the SVG attributes of the new marker + """ + output = copy.deepcopy(_marker_templates[shape]) + for i in output.sub: + i.attr.update(attr_preprocess(attr)) + output["id"] = id + return output + + +class Line(Curve): + """Draws a line between two points. + + Line(x1, y1, x2, y2, arrow_start, arrow_end, attribute=value) + + x1, y1 required the starting point + x2, y2 required the ending point + arrow_start default=None if an identifier string/Unicode, + draw a new arrow object at the + beginning of the line; if a marker, + draw that marker instead + arrow_end default=None same for the end of the line + attribute=value pairs keyword list SVG attributes + """ + defaults = {} + + def __repr__(self): + return "" % ( + self.x1, self.y1, self.x2, self.y2, self.attr) + + def __init__(self, x1, y1, x2, y2, arrow_start=None, arrow_end=None, **attr): + self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2 + self.arrow_start, self.arrow_end = arrow_start, arrow_end + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + + line = self.Path(trans).SVG() + + if ((self.arrow_start != False and self.arrow_start is not None) or + (self.arrow_end != False and self.arrow_end is not None)): + defs = SVG("defs") + + if self.arrow_start != False and self.arrow_start is not None: + if isinstance(self.arrow_start, SVG): + defs.append(self.arrow_start) + line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"] + elif isinstance(self.arrow_start, basestring): + defs.append(make_marker(self.arrow_start, "arrow_start")) + line.attr["marker-start"] = "url(#%s)" % self.arrow_start + else: + raise TypeError, "arrow_start must be False/None or an id string for the new marker" + + if self.arrow_end != False and self.arrow_end is not None: + if isinstance(self.arrow_end, SVG): + defs.append(self.arrow_end) + line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"] + elif isinstance(self.arrow_end, basestring): + defs.append(make_marker(self.arrow_end, "arrow_end")) + line.attr["marker-end"] = "url(#%s)" % self.arrow_end + else: + raise TypeError, "arrow_end must be False/None or an id string for the new marker" + + return SVG("g", defs, line) + + return line + + def Path(self, trans=None, local=False): + """Apply the transformation "trans" and return a Path object in + global coordinates. If local=True, return a Path in local coordinates + (which must be transformed again).""" + self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1 + t*(self.y2 - self.y1)) + self.low = 0. + self.high = 1. + self.loop = False + + if trans is None: + return Path([("M", self.x1, self.y1, not local), ("L", self.x2, self.y2, not local)], **self.attr) + else: + return Curve.Path(self, trans, local) + + +class LineGlobal: + """Draws a line between two points, one or both of which is in + global coordinates. + + Line(x1, y1, x2, y2, lcoal1, local2, arrow_start, arrow_end, attribute=value) + + x1, y1 required the starting point + x2, y2 required the ending point + local1 default=False if True, interpret first point as a + local coordinate (apply transform) + local2 default=False if True, interpret second point as a + local coordinate (apply transform) + arrow_start default=None if an identifier string/Unicode, + draw a new arrow object at the + beginning of the line; if a marker, + draw that marker instead + arrow_end default=None same for the end of the line + attribute=value pairs keyword list SVG attributes + """ + defaults = {} + + def __repr__(self): + local1, local2 = "", "" + if self.local1: + local1 = "L" + if self.local2: + local2 = "L" + + return "" % ( + local1, str(self.x1), str(self.y1), local2, str(self.x2), str(self.y2), self.attr) + + def __init__(self, x1, y1, x2, y2, local1=False, local2=False, arrow_start=None, arrow_end=None, **attr): + self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2 + self.local1, self.local2 = local1, local2 + self.arrow_start, self.arrow_end = arrow_start, arrow_end + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + if isinstance(trans, basestring): + trans = totrans(trans) + + X1, Y1, X2, Y2 = self.x1, self.y1, self.x2, self.y2 + + if self.local1: + X1, Y1 = trans(X1, Y1) + if self.local2: + X2, Y2 = trans(X2, Y2) + + line = SVG("path", d="M%s %s L%s %s" % (X1, Y1, X2, Y2), **self.attr) + + if ((self.arrow_start != False and self.arrow_start is not None) or + (self.arrow_end != False and self.arrow_end is not None)): + defs = SVG("defs") + + if self.arrow_start != False and self.arrow_start is not None: + if isinstance(self.arrow_start, SVG): + defs.append(self.arrow_start) + line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"] + elif isinstance(self.arrow_start, basestring): + defs.append(make_marker(self.arrow_start, "arrow_start")) + line.attr["marker-start"] = "url(#%s)" % self.arrow_start + else: + raise TypeError, "arrow_start must be False/None or an id string for the new marker" + + if self.arrow_end != False and self.arrow_end is not None: + if isinstance(self.arrow_end, SVG): + defs.append(self.arrow_end) + line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"] + elif isinstance(self.arrow_end, basestring): + defs.append(make_marker(self.arrow_end, "arrow_end")) + line.attr["marker-end"] = "url(#%s)" % self.arrow_end + else: + raise TypeError, "arrow_end must be False/None or an id string for the new marker" + + return SVG("g", defs, line) + + return line + + +class VLine(Line): + """Draws a vertical line. + + VLine(y1, y2, x, attribute=value) + + y1, y2 required y range + x required x position + attribute=value pairs keyword list SVG attributes + """ + defaults = {} + + def __repr__(self): + return "" % (self.y1, self.y2, self.x, self.attr) + + def __init__(self, y1, y2, x, **attr): + self.x = x + self.attr = dict(self.defaults) + self.attr.update(attr) + Line.__init__(self, x, y1, x, y2, **self.attr) + + def Path(self, trans=None, local=False): + """Apply the transformation "trans" and return a Path object in + global coordinates. If local=True, return a Path in local coordinates + (which must be transformed again).""" + self.x1 = self.x + self.x2 = self.x + return Line.Path(self, trans, local) + + +class HLine(Line): + """Draws a horizontal line. + + HLine(x1, x2, y, attribute=value) + + x1, x2 required x range + y required y position + attribute=value pairs keyword list SVG attributes + """ + defaults = {} + + def __repr__(self): + return "" % (self.x1, self.x2, self.y, self.attr) + + def __init__(self, x1, x2, y, **attr): + self.y = y + self.attr = dict(self.defaults) + self.attr.update(attr) + Line.__init__(self, x1, y, x2, y, **self.attr) + + def Path(self, trans=None, local=False): + """Apply the transformation "trans" and return a Path object in + global coordinates. If local=True, return a Path in local coordinates + (which must be transformed again).""" + self.y1 = self.y + self.y2 = self.y + return Line.Path(self, trans, local) + +###################################################################### + +class Rect(Curve): + """Draws a rectangle. + + Rect(x1, y1, x2, y2, attribute=value) + + x1, y1 required the starting point + x2, y2 required the ending point + attribute=value pairs keyword list SVG attributes + """ + defaults = {} + + def __repr__(self): + return "" % ( + self.x1, self.y1, self.x2, self.y2, self.attr) + + def __init__(self, x1, y1, x2, y2, **attr): + self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2 + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + return self.Path(trans).SVG() + + def Path(self, trans=None, local=False): + """Apply the transformation "trans" and return a Path object in + global coordinates. If local=True, return a Path in local coordinates + (which must be transformed again).""" + if trans is None: + 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) + + else: + self.low = 0. + self.high = 1. + self.loop = False + + self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1) + d1 = Curve.Path(self, trans, local).d + + self.f = lambda t: (self.x2, self.y1 + t*(self.y2 - self.y1)) + d2 = Curve.Path(self, trans, local).d + del d2[0] + + self.f = lambda t: (self.x2 + t*(self.x1 - self.x2), self.y2) + d3 = Curve.Path(self, trans, local).d + del d3[0] + + self.f = lambda t: (self.x1, self.y2 + t*(self.y1 - self.y2)) + d4 = Curve.Path(self, trans, local).d + del d4[0] + + return Path(d=(d1 + d2 + d3 + d4 + [("Z",)]), **self.attr) + +###################################################################### + +class Ellipse(Curve): + """Draws an ellipse from a semimajor vector (ax,ay) and a semiminor + length (b). + + Ellipse(x, y, ax, ay, b, attribute=value) + + x, y required the center of the ellipse/circle + ax, ay required a vector indicating the length + and direction of the semimajor axis + b required the length of the semiminor axis. + If equal to sqrt(ax2 + ay2), the + ellipse is a circle + attribute=value pairs keyword list SVG attributes + + (If sqrt(ax**2 + ay**2) is less than b, then (ax,ay) is actually the + semiminor axis.) + """ + defaults = {} + + def __repr__(self): + return "" % ( + self.x, self.y, self.ax, self.ay, self.b, self.attr) + + def __init__(self, x, y, ax, ay, b, **attr): + self.x, self.y, self.ax, self.ay, self.b = x, y, ax, ay, b + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + return self.Path(trans).SVG() + + def Path(self, trans=None, local=False): + """Apply the transformation "trans" and return a Path object in + global coordinates. If local=True, return a Path in local coordinates + (which must be transformed again).""" + angle = math.atan2(self.ay, self.ax) + math.pi/2. + bx = self.b * math.cos(angle) + by = self.b * math.sin(angle) + + 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)) + self.low = -math.pi + self.high = math.pi + self.loop = True + return Curve.Path(self, trans, local) + +###################################################################### + +def unumber(x): + """Converts numbers to a Unicode string, taking advantage of special + Unicode characters to make nice minus signs and scientific notation. + """ + output = u"%g" % x + + if output[0] == u"-": + output = u"\u2013" + output[1:] + + index = output.find(u"e") + if index != -1: + uniout = unicode(output[:index]) + u"\u00d710" + saw_nonzero = False + for n in output[index+1:]: + if n == u"+": + pass # uniout += u"\u207a" + elif n == u"-": + uniout += u"\u207b" + elif n == u"0": + if saw_nonzero: + uniout += u"\u2070" + elif n == u"1": + saw_nonzero = True + uniout += u"\u00b9" + elif n == u"2": + saw_nonzero = True + uniout += u"\u00b2" + elif n == u"3": + saw_nonzero = True + uniout += u"\u00b3" + elif u"4" <= n <= u"9": + saw_nonzero = True + if saw_nonzero: + uniout += eval("u\"\\u%x\"" % (0x2070 + ord(n) - ord(u"0"))) + else: + uniout += n + + if uniout[:2] == u"1\u00d7": + uniout = uniout[2:] + return uniout + + return output + + +class Ticks: + """Superclass for all graphics primitives that draw ticks, + miniticks, and tick labels. This class only draws the ticks. + + Ticks(f, low, high, ticks, miniticks, labels, logbase, arrow_start, + arrow_end, text_attr, attribute=value) + + f required parametric function along which ticks + will be drawn; has the same format as + the function used in Curve + low, high required range of the independent variable + ticks default=-10 request ticks according to the standard + tick specification (see below) + miniticks default=True request miniticks according to the + standard minitick specification (below) + labels True request tick labels according to the + standard tick label specification (below) + logbase default=None if a number, the axis is logarithmic with + ticks at the given base (usually 10) + arrow_start default=None if a new string identifier, draw an arrow + at the low-end of the axis, referenced by + that identifier; if an SVG marker object, + use that marker + arrow_end default=None if a new string identifier, draw an arrow + at the high-end of the axis, referenced by + that identifier; if an SVG marker object, + use that marker + text_attr default={} SVG attributes for the text labels + attribute=value pairs keyword list SVG attributes for the tick marks + + Standard tick specification: + + * True: same as -10 (below). + * Positive number N: draw exactly N ticks, including the endpoints. To + subdivide an axis into 10 equal-sized segments, ask for 11 ticks. + * Negative number -N: draw at least N ticks. Ticks will be chosen with + "natural" values, multiples of 2 or 5. + * List of values: draw a tick mark at each value. + * Dict of value, label pairs: draw a tick mark at each value, labeling + it with the given string. This lets you say things like {3.14159: "pi"}. + * False or None: no ticks. + + Standard minitick specification: + + * True: draw miniticks with "natural" values, more closely spaced than + the ticks. + * Positive number N: draw exactly N miniticks, including the endpoints. + To subdivide an axis into 100 equal-sized segments, ask for 101 miniticks. + * Negative number -N: draw at least N miniticks. + * List of values: draw a minitick mark at each value. + * False or None: no miniticks. + + Standard tick label specification: + + * True: use the unumber function (described below) + * Format string: standard format strings, e.g. "%5.2f" for 12.34 + * Python callable: function that converts numbers to strings + * False or None: no labels + """ + defaults = {"stroke-width": "0.25pt", } + text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, } + tick_start = -1.5 + tick_end = 1.5 + minitick_start = -0.75 + minitick_end = 0.75 + text_start = 2.5 + text_angle = 0. + + def __repr__(self): + return "" % ( + self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr) + + def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None, + arrow_start=None, arrow_end=None, text_attr={}, **attr): + self.f = f + self.low = low + self.high = high + self.ticks = ticks + self.miniticks = miniticks + self.labels = labels + self.logbase = logbase + self.arrow_start = arrow_start + self.arrow_end = arrow_end + + self.attr = dict(self.defaults) + self.attr.update(attr) + + self.text_attr = dict(self.text_defaults) + self.text_attr.update(text_attr) + + def orient_tickmark(self, t, trans=None): + """Return the position, normalized local x vector, normalized + local y vector, and angle of a tick at position t. + + Normally only used internally. + """ + if isinstance(trans, basestring): + trans = totrans(trans) + if trans is None: + f = self.f + else: + f = lambda t: trans(*self.f(t)) + + eps = _epsilon * abs(self.high - self.low) + + X, Y = f(t) + Xprime, Yprime = f(t + eps) + xhatx, xhaty = (Xprime - X)/eps, (Yprime - Y)/eps + + norm = math.sqrt(xhatx**2 + xhaty**2) + if norm != 0: + xhatx, xhaty = xhatx/norm, xhaty/norm + else: + xhatx, xhaty = 1., 0. + + angle = math.atan2(xhaty, xhatx) + math.pi/2. + yhatx, yhaty = math.cos(angle), math.sin(angle) + + return (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + if isinstance(trans, basestring): + trans = totrans(trans) + + self.last_ticks, self.last_miniticks = self.interpret() + tickmarks = Path([], **self.attr) + minitickmarks = Path([], **self.attr) + output = SVG("g") + + if ((self.arrow_start != False and self.arrow_start is not None) or + (self.arrow_end != False and self.arrow_end is not None)): + defs = SVG("defs") + + if self.arrow_start != False and self.arrow_start is not None: + if isinstance(self.arrow_start, SVG): + defs.append(self.arrow_start) + elif isinstance(self.arrow_start, basestring): + defs.append(make_marker(self.arrow_start, "arrow_start")) + else: + raise TypeError, "arrow_start must be False/None or an id string for the new marker" + + if self.arrow_end != False and self.arrow_end is not None: + if isinstance(self.arrow_end, SVG): + defs.append(self.arrow_end) + elif isinstance(self.arrow_end, basestring): + defs.append(make_marker(self.arrow_end, "arrow_end")) + else: + raise TypeError, "arrow_end must be False/None or an id string for the new marker" + + output.append(defs) + + eps = _epsilon * (self.high - self.low) + + for t, label in self.last_ticks.items(): + (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans) + + if ((not self.arrow_start or abs(t - self.low) > eps) and + (not self.arrow_end or abs(t - self.high) > eps)): + tickmarks.d.append(("M", X - yhatx*self.tick_start, Y - yhaty*self.tick_start, True)) + tickmarks.d.append(("L", X - yhatx*self.tick_end, Y - yhaty*self.tick_end, True)) + + angle = (angle - math.pi/2.)*180./math.pi + self.text_angle + + ########### a HACK! ############ (to be removed when Inkscape handles baselines) + if _hacks["inkscape-text-vertical-shift"]: + if self.text_start > 0: + X += math.cos(angle*math.pi/180. + math.pi/2.) * 2. + Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2. + else: + X += math.cos(angle*math.pi/180. + math.pi/2.) * 2. * 2.5 + Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2. * 2.5 + ########### end hack ########### + + if label != "": + output.append(SVG("text", label, transform="translate(%g, %g) rotate(%g)" % + (X - yhatx*self.text_start, Y - yhaty*self.text_start, angle), **self.text_attr)) + + for t in self.last_miniticks: + skip = False + for tt in self.last_ticks.keys(): + if abs(t - tt) < eps: + skip = True + break + if not skip: + (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans) + + if ((not self.arrow_start or abs(t - self.low) > eps) and + (not self.arrow_end or abs(t - self.high) > eps)): + minitickmarks.d.append(("M", X - yhatx*self.minitick_start, Y - yhaty*self.minitick_start, True)) + minitickmarks.d.append(("L", X - yhatx*self.minitick_end, Y - yhaty*self.minitick_end, True)) + + output.prepend(tickmarks.SVG(trans)) + output.prepend(minitickmarks.SVG(trans)) + return output + + def interpret(self): + """Evaluate and return optimal ticks and miniticks according to + the standard minitick specification. + + Normally only used internally. + """ + + if self.labels is None or self.labels == False: + format = lambda x: "" + + elif self.labels == True: + format = unumber + + elif isinstance(self.labels, basestring): + format = lambda x: (self.labels % x) + + elif callable(self.labels): + format = self.labels + + else: + raise TypeError, "labels must be None/False, True, a format string, or a number->string function" + + # Now for the ticks + ticks = self.ticks + + # Case 1: ticks is None/False + if ticks is None or ticks == False: + return {}, [] + + # Case 2: ticks is the number of desired ticks + elif isinstance(ticks, (int, long)): + if ticks == True: + ticks = -10 + + if self.logbase is None: + ticks = self.compute_ticks(ticks, format) + else: + ticks = self.compute_logticks(self.logbase, ticks, format) + + # Now for the miniticks + if self.miniticks == True: + if self.logbase is None: + return ticks, self.compute_miniticks(ticks) + else: + return ticks, self.compute_logminiticks(self.logbase) + + elif isinstance(self.miniticks, (int, long)): + return ticks, self.regular_miniticks(self.miniticks) + + elif getattr(self.miniticks, "__iter__", False): + return ticks, self.miniticks + + elif self.miniticks == False or self.miniticks is None: + return ticks, [] + + else: + raise TypeError, "miniticks must be None/False, True, a number of desired miniticks, or a list of numbers" + + # Cases 3 & 4: ticks is iterable + elif getattr(ticks, "__iter__", False): + + # Case 3: ticks is some kind of list + if not isinstance(ticks, dict): + output = {} + eps = _epsilon * (self.high - self.low) + for x in ticks: + if format == unumber and abs(x) < eps: + output[x] = u"0" + else: + output[x] = format(x) + ticks = output + + # Case 4: ticks is a dict + else: + pass + + # Now for the miniticks + if self.miniticks == True: + if self.logbase is None: + return ticks, self.compute_miniticks(ticks) + else: + return ticks, self.compute_logminiticks(self.logbase) + + elif isinstance(self.miniticks, (int, long)): + return ticks, self.regular_miniticks(self.miniticks) + + elif getattr(self.miniticks, "__iter__", False): + return ticks, self.miniticks + + elif self.miniticks == False or self.miniticks is None: + return ticks, [] + + else: + raise TypeError, "miniticks must be None/False, True, a number of desired miniticks, or a list of numbers" + + else: + raise TypeError, "ticks must be None/False, a number of desired ticks, a list of numbers, or a dictionary of explicit markers" + + def compute_ticks(self, N, format): + """Return less than -N or exactly N optimal linear ticks. + + Normally only used internally. + """ + if self.low >= self.high: + raise ValueError, "low must be less than high" + if N == 1: + raise ValueError, "N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum" + + eps = _epsilon * (self.high - self.low) + + if N >= 0: + output = {} + x = self.low + for i in xrange(N): + if format == unumber and abs(x) < eps: + label = u"0" + else: + label = format(x) + output[x] = label + x += (self.high - self.low)/(N-1.) + return output + + N = -N + + counter = 0 + granularity = 10**math.ceil(math.log10(max(abs(self.low), abs(self.high)))) + lowN = math.ceil(1.*self.low / granularity) + highN = math.floor(1.*self.high / granularity) + + while lowN > highN: + countermod3 = counter % 3 + if countermod3 == 0: + granularity *= 0.5 + elif countermod3 == 1: + granularity *= 0.4 + else: + granularity *= 0.5 + counter += 1 + lowN = math.ceil(1.*self.low / granularity) + highN = math.floor(1.*self.high / granularity) + + last_granularity = granularity + last_trial = None + + while True: + trial = {} + for n in range(int(lowN), int(highN)+1): + x = n * granularity + if format == unumber and abs(x) < eps: + label = u"0" + else: + label = format(x) + trial[x] = label + + if int(highN)+1 - int(lowN) >= N: + if last_trial is None: + v1, v2 = self.low, self.high + return {v1: format(v1), v2: format(v2)} + else: + low_in_ticks, high_in_ticks = False, False + for t in last_trial.keys(): + if 1.*abs(t - self.low)/last_granularity < _epsilon: + low_in_ticks = True + if 1.*abs(t - self.high)/last_granularity < _epsilon: + high_in_ticks = True + + lowN = 1.*self.low / last_granularity + highN = 1.*self.high / last_granularity + if abs(lowN - round(lowN)) < _epsilon and not low_in_ticks: + last_trial[self.low] = format(self.low) + if abs(highN - round(highN)) < _epsilon and not high_in_ticks: + last_trial[self.high] = format(self.high) + return last_trial + + last_granularity = granularity + last_trial = trial + + countermod3 = counter % 3 + if countermod3 == 0: + granularity *= 0.5 + elif countermod3 == 1: + granularity *= 0.4 + else: + granularity *= 0.5 + counter += 1 + lowN = math.ceil(1.*self.low / granularity) + highN = math.floor(1.*self.high / granularity) + + def regular_miniticks(self, N): + """Return exactly N linear ticks. + + Normally only used internally. + """ + output = [] + x = self.low + for i in xrange(N): + output.append(x) + x += (self.high - self.low)/(N-1.) + return output + + def compute_miniticks(self, original_ticks): + """Return optimal linear miniticks, given a set of ticks. + + Normally only used internally. + """ + if len(original_ticks) < 2: + original_ticks = ticks(self.low, self.high) # XXX ticks is undefined! + original_ticks = original_ticks.keys() + original_ticks.sort() + + if self.low > original_ticks[0] + _epsilon or self.high < original_ticks[-1] - _epsilon: + raise ValueError, "original_ticks {%g...%g} extend beyond [%g, %g]" % (original_ticks[0], original_ticks[-1], self.low, self.high) + + granularities = [] + for i in range(len(original_ticks)-1): + granularities.append(original_ticks[i+1] - original_ticks[i]) + spacing = 10**(math.ceil(math.log10(min(granularities)) - 1)) + + output = [] + x = original_ticks[0] - math.ceil(1.*(original_ticks[0] - self.low) / spacing) * spacing + + while x <= self.high: + if x >= self.low: + already_in_ticks = False + for t in original_ticks: + if abs(x-t) < _epsilon * (self.high - self.low): + already_in_ticks = True + if not already_in_ticks: + output.append(x) + x += spacing + return output + + def compute_logticks(self, base, N, format): + """Return less than -N or exactly N optimal logarithmic ticks. + + Normally only used internally. + """ + if self.low >= self.high: + raise ValueError, "low must be less than high" + if N == 1: + raise ValueError, "N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum" + + eps = _epsilon * (self.high - self.low) + + if N >= 0: + output = {} + x = self.low + for i in xrange(N): + if format == unumber and abs(x) < eps: + label = u"0" + else: + label = format(x) + output[x] = label + x += (self.high - self.low)/(N-1.) + return output + + N = -N + + lowN = math.floor(math.log(self.low, base)) + highN = math.ceil(math.log(self.high, base)) + output = {} + for n in range(int(lowN), int(highN)+1): + x = base**n + label = format(x) + if self.low <= x <= self.high: + output[x] = label + + for i in range(1, len(output)): + keys = output.keys() + keys.sort() + keys = keys[::i] + values = map(lambda k: output[k], keys) + if len(values) <= N: + for k in output.keys(): + if k not in keys: + output[k] = "" + break + + if len(output) <= 2: + output2 = self.compute_ticks(N=-int(math.ceil(N/2.)), format=format) + lowest = min(output2) + + for k in output: + if k < lowest: + output2[k] = output[k] + output = output2 + + return output + + def compute_logminiticks(self, base): + """Return optimal logarithmic miniticks, given a set of ticks. + + Normally only used internally. + """ + if self.low >= self.high: + raise ValueError, "low must be less than high" + + lowN = math.floor(math.log(self.low, base)) + highN = math.ceil(math.log(self.high, base)) + output = [] + num_ticks = 0 + for n in range(int(lowN), int(highN)+1): + x = base**n + if self.low <= x <= self.high: + num_ticks += 1 + for m in range(2, int(math.ceil(base))): + minix = m * x + if self.low <= minix <= self.high: + output.append(minix) + + if num_ticks <= 2: + return [] + else: + return output + +###################################################################### + +class CurveAxis(Curve, Ticks): + """Draw an axis with tick marks along a parametric curve. + + CurveAxis(f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end, + text_attr, attribute=value) + + f required a Python callable or string in + the form "f(t), g(t)", just like Curve + low, high required left and right endpoints + ticks default=-10 request ticks according to the standard + tick specification (see help(Ticks)) + miniticks default=True request miniticks according to the + standard minitick specification + labels True request tick labels according to the + standard tick label specification + logbase default=None if a number, the x axis is logarithmic + with ticks at the given base (10 being + the most common) + arrow_start default=None if a new string identifier, draw an + arrow at the low-end of the axis, + referenced by that identifier; if an + SVG marker object, use that marker + arrow_end default=None if a new string identifier, draw an + arrow at the high-end of the axis, + referenced by that identifier; if an + SVG marker object, use that marker + text_attr default={} SVG attributes for the text labels + attribute=value pairs keyword list SVG attributes + """ + defaults = {"stroke-width": "0.25pt", } + text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, } + + def __repr__(self): + return "" % ( + self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr) + + def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None, + arrow_start=None, arrow_end=None, text_attr={}, **attr): + tattr = dict(self.text_defaults) + tattr.update(text_attr) + Curve.__init__(self, f, low, high) + Ticks.__init__(self, f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + func = Curve.SVG(self, trans) + ticks = Ticks.SVG(self, trans) # returns a + + if self.arrow_start != False and self.arrow_start is not None: + if isinstance(self.arrow_start, basestring): + func.attr["marker-start"] = "url(#%s)" % self.arrow_start + else: + func.attr["marker-start"] = "url(#%s)" % self.arrow_start.id + + if self.arrow_end != False and self.arrow_end is not None: + if isinstance(self.arrow_end, basestring): + func.attr["marker-end"] = "url(#%s)" % self.arrow_end + else: + func.attr["marker-end"] = "url(#%s)" % self.arrow_end.id + + ticks.append(func) + return ticks + + +class LineAxis(Line, Ticks): + """Draws an axis with tick marks along a line. + + LineAxis(x1, y1, x2, y2, start, end, ticks, miniticks, labels, logbase, + arrow_start, arrow_end, text_attr, attribute=value) + + x1, y1 required starting point + x2, y2 required ending point + start, end default=0, 1 values to start and end labeling + ticks default=-10 request ticks according to the standard + tick specification (see help(Ticks)) + miniticks default=True request miniticks according to the + standard minitick specification + labels True request tick labels according to the + standard tick label specification + logbase default=None if a number, the x axis is logarithmic + with ticks at the given base (usually 10) + arrow_start default=None if a new string identifier, draw an arrow + at the low-end of the axis, referenced by + that identifier; if an SVG marker object, + use that marker + arrow_end default=None if a new string identifier, draw an arrow + at the high-end of the axis, referenced by + that identifier; if an SVG marker object, + use that marker + text_attr default={} SVG attributes for the text labels + attribute=value pairs keyword list SVG attributes + """ + defaults = {"stroke-width": "0.25pt", } + text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, } + + def __repr__(self): + return "" % ( + self.x1, self.y1, self.x2, self.y2, str(self.ticks), str(self.labels), self.attr) + + def __init__(self, x1, y1, x2, y2, start=0., end=1., ticks=-10, miniticks=True, labels=True, + logbase=None, arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr): + self.start = start + self.end = end + self.exclude = exclude + tattr = dict(self.text_defaults) + tattr.update(text_attr) + Line.__init__(self, x1, y1, x2, y2, **attr) + Ticks.__init__(self, None, None, None, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr) + + def interpret(self): + if self.exclude is not None and not (isinstance(self.exclude, (tuple, list)) and len(self.exclude) == 2 and + isinstance(self.exclude[0], (int, long, float)) and isinstance(self.exclude[1], (int, long, float))): + raise TypeError, "exclude must either be None or (low, high)" + + ticks, miniticks = Ticks.interpret(self) + if self.exclude is None: + return ticks, miniticks + + ticks2 = {} + for loc, label in ticks.items(): + if self.exclude[0] <= loc <= self.exclude[1]: + ticks2[loc] = "" + else: + ticks2[loc] = label + + return ticks2, miniticks + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + line = Line.SVG(self, trans) # must be evaluated first, to set self.f, self.low, self.high + + f01 = self.f + self.f = lambda t: f01(1. * (t - self.start) / (self.end - self.start)) + self.low = self.start + self.high = self.end + + if self.arrow_start != False and self.arrow_start is not None: + if isinstance(self.arrow_start, basestring): + line.attr["marker-start"] = "url(#%s)" % self.arrow_start + else: + line.attr["marker-start"] = "url(#%s)" % self.arrow_start.id + + if self.arrow_end != False and self.arrow_end is not None: + if isinstance(self.arrow_end, basestring): + line.attr["marker-end"] = "url(#%s)" % self.arrow_end + else: + line.attr["marker-end"] = "url(#%s)" % self.arrow_end.id + + ticks = Ticks.SVG(self, trans) # returns a + ticks.append(line) + return ticks + + +class XAxis(LineAxis): + """Draws an x axis with tick marks. + + XAxis(xmin, xmax, aty, ticks, miniticks, labels, logbase, arrow_start, arrow_end, + exclude, text_attr, attribute=value) + + xmin, xmax required the x range + aty default=0 y position to draw the axis + ticks default=-10 request ticks according to the standard + tick specification (see help(Ticks)) + miniticks default=True request miniticks according to the + standard minitick specification + labels True request tick labels according to the + standard tick label specification + logbase default=None if a number, the x axis is logarithmic + with ticks at the given base (usually 10) + arrow_start default=None if a new string identifier, draw an arrow + at the low-end of the axis, referenced by + that identifier; if an SVG marker object, + use that marker + arrow_end default=None if a new string identifier, draw an arrow + at the high-end of the axis, referenced by + that identifier; if an SVG marker object, + use that marker + exclude default=None if a (low, high) pair, don't draw text + labels within this range + text_attr default={} SVG attributes for the text labels + attribute=value pairs keyword list SVG attributes for all lines + + The exclude option is provided for Axes to keep text from overlapping + where the axes cross. Normal users are not likely to need it. + """ + defaults = {"stroke-width": "0.25pt", } + text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, "dominant-baseline": "text-before-edge", } + text_start = -1. + text_angle = 0. + + def __repr__(self): + return "" % ( + self.xmin, self.xmax, self.aty, str(self.ticks), str(self.labels), self.attr) # XXX self.xmin/xmax undefd! + + def __init__(self, xmin, xmax, aty=0, ticks=-10, miniticks=True, labels=True, logbase=None, + arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr): + self.aty = aty + tattr = dict(self.text_defaults) + tattr.update(text_attr) + LineAxis.__init__(self, xmin, aty, xmax, aty, xmin, xmax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + self.y1 = self.aty + self.y2 = self.aty + return LineAxis.SVG(self, trans) + + +class YAxis(LineAxis): + """Draws a y axis with tick marks. + + YAxis(ymin, ymax, atx, ticks, miniticks, labels, logbase, arrow_start, arrow_end, + exclude, text_attr, attribute=value) + + ymin, ymax required the y range + atx default=0 x position to draw the axis + ticks default=-10 request ticks according to the standard + tick specification (see help(Ticks)) + miniticks default=True request miniticks according to the + standard minitick specification + labels True request tick labels according to the + standard tick label specification + logbase default=None if a number, the y axis is logarithmic + with ticks at the given base (usually 10) + arrow_start default=None if a new string identifier, draw an arrow + at the low-end of the axis, referenced by + that identifier; if an SVG marker object, + use that marker + arrow_end default=None if a new string identifier, draw an arrow + at the high-end of the axis, referenced by + that identifier; if an SVG marker object, + use that marker + exclude default=None if a (low, high) pair, don't draw text + labels within this range + text_attr default={} SVG attributes for the text labels + attribute=value pairs keyword list SVG attributes for all lines + + The exclude option is provided for Axes to keep text from overlapping + where the axes cross. Normal users are not likely to need it. + """ + defaults = {"stroke-width": "0.25pt", } + text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, "text-anchor": "end", "dominant-baseline": "middle", } + text_start = 2.5 + text_angle = 90. + + def __repr__(self): + return "" % ( + self.ymin, self.ymax, self.atx, str(self.ticks), str(self.labels), self.attr) # XXX self.ymin/ymax undefd! + + def __init__(self, ymin, ymax, atx=0, ticks=-10, miniticks=True, labels=True, logbase=None, + arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr): + self.atx = atx + tattr = dict(self.text_defaults) + tattr.update(text_attr) + LineAxis.__init__(self, atx, ymin, atx, ymax, ymin, ymax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + self.x1 = self.atx + self.x2 = self.atx + return LineAxis.SVG(self, trans) + + +class Axes: + """Draw a pair of intersecting x-y axes. + + Axes(xmin, xmax, ymin, ymax, atx, aty, xticks, xminiticks, xlabels, xlogbase, + yticks, yminiticks, ylabels, ylogbase, arrows, text_attr, attribute=value) + + xmin, xmax required the x range + ymin, ymax required the y range + atx, aty default=0, 0 point where the axes try to cross; + if outside the range, the axes will + cross at the closest corner + xticks default=-10 request ticks according to the standard + tick specification (see help(Ticks)) + xminiticks default=True request miniticks according to the + standard minitick specification + xlabels True request tick labels according to the + standard tick label specification + xlogbase default=None if a number, the x axis is logarithmic + with ticks at the given base (usually 10) + yticks default=-10 request ticks according to the standard + tick specification + yminiticks default=True request miniticks according to the + standard minitick specification + ylabels True request tick labels according to the + standard tick label specification + ylogbase default=None if a number, the y axis is logarithmic + with ticks at the given base (usually 10) + arrows default=None if a new string identifier, draw arrows + referenced by that identifier + text_attr default={} SVG attributes for the text labels + attribute=value pairs keyword list SVG attributes for all lines + """ + defaults = {"stroke-width": "0.25pt", } + text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, } + + def __repr__(self): + return "" % ( + self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty, self.attr) + + def __init__(self, xmin, xmax, ymin, ymax, atx=0, aty=0, + xticks=-10, xminiticks=True, xlabels=True, xlogbase=None, + yticks=-10, yminiticks=True, ylabels=True, ylogbase=None, + arrows=None, text_attr={}, **attr): + self.xmin, self.xmax = xmin, xmax + self.ymin, self.ymax = ymin, ymax + self.atx, self.aty = atx, aty + self.xticks, self.xminiticks, self.xlabels, self.xlogbase = xticks, xminiticks, xlabels, xlogbase + self.yticks, self.yminiticks, self.ylabels, self.ylogbase = yticks, yminiticks, ylabels, ylogbase + self.arrows = arrows + + self.text_attr = dict(self.text_defaults) + self.text_attr.update(text_attr) + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + atx, aty = self.atx, self.aty + if atx < self.xmin: + atx = self.xmin + if atx > self.xmax: + atx = self.xmax + if aty < self.ymin: + aty = self.ymin + if aty > self.ymax: + aty = self.ymax + + xmargin = 0.1 * abs(self.ymin - self.ymax) + xexclude = atx - xmargin, atx + xmargin + + ymargin = 0.1 * abs(self.xmin - self.xmax) + yexclude = aty - ymargin, aty + ymargin + + if self.arrows is not None and self.arrows != False: + xarrow_start = self.arrows + ".xstart" + xarrow_end = self.arrows + ".xend" + yarrow_start = self.arrows + ".ystart" + yarrow_end = self.arrows + ".yend" + else: + xarrow_start = xarrow_end = yarrow_start = yarrow_end = None + + 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) + 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) + return SVG("g", *(xaxis.sub + yaxis.sub)) + +###################################################################### + +class HGrid(Ticks): + """Draws the horizontal lines of a grid over a specified region + using the standard tick specification (see help(Ticks)) to place the + grid lines. + + HGrid(xmin, xmax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value) + + xmin, xmax required the x range + low, high required the y range + ticks default=-10 request ticks according to the standard + tick specification (see help(Ticks)) + miniticks default=False request miniticks according to the + standard minitick specification + logbase default=None if a number, the axis is logarithmic + with ticks at the given base (usually 10) + mini_attr default={} SVG attributes for the minitick-lines + (if miniticks != False) + attribute=value pairs keyword list SVG attributes for the major tick lines + """ + defaults = {"stroke-width": "0.25pt", "stroke": "gray", } + mini_defaults = {"stroke-width": "0.25pt", "stroke": "lightgray", "stroke-dasharray": "1,1", } + + def __repr__(self): + return "" % ( + self.xmin, self.xmax, self.low, self.high, str(self.ticks), str(self.miniticks), self.attr) + + def __init__(self, xmin, xmax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr): + self.xmin, self.xmax = xmin, xmax + + self.mini_attr = dict(self.mini_defaults) + self.mini_attr.update(mini_attr) + + Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase) + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + self.last_ticks, self.last_miniticks = Ticks.interpret(self) + + ticksd = [] + for t in self.last_ticks.keys(): + ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d + + miniticksd = [] + for t in self.last_miniticks: + miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d + + return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG()) + + +class VGrid(Ticks): + """Draws the vertical lines of a grid over a specified region + using the standard tick specification (see help(Ticks)) to place the + grid lines. + + HGrid(ymin, ymax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value) + + ymin, ymax required the y range + low, high required the x range + ticks default=-10 request ticks according to the standard + tick specification (see help(Ticks)) + miniticks default=False request miniticks according to the + standard minitick specification + logbase default=None if a number, the axis is logarithmic + with ticks at the given base (usually 10) + mini_attr default={} SVG attributes for the minitick-lines + (if miniticks != False) + attribute=value pairs keyword list SVG attributes for the major tick lines + """ + defaults = {"stroke-width": "0.25pt", "stroke": "gray", } + mini_defaults = {"stroke-width": "0.25pt", "stroke": "lightgray", "stroke-dasharray": "1,1", } + + def __repr__(self): + return "" % ( + self.ymin, self.ymax, self.low, self.high, str(self.ticks), str(self.miniticks), self.attr) + + def __init__(self, ymin, ymax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr): + self.ymin, self.ymax = ymin, ymax + + self.mini_attr = dict(self.mini_defaults) + self.mini_attr.update(mini_attr) + + Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase) + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + self.last_ticks, self.last_miniticks = Ticks.interpret(self) + + ticksd = [] + for t in self.last_ticks.keys(): + ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d + + miniticksd = [] + for t in self.last_miniticks: + miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d + + return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG()) + + +class Grid(Ticks): + """Draws a grid over a specified region using the standard tick + specification (see help(Ticks)) to place the grid lines. + + Grid(xmin, xmax, ymin, ymax, ticks, miniticks, logbase, mini_attr, attribute=value) + + xmin, xmax required the x range + ymin, ymax required the y range + ticks default=-10 request ticks according to the standard + tick specification (see help(Ticks)) + miniticks default=False request miniticks according to the + standard minitick specification + logbase default=None if a number, the axis is logarithmic + with ticks at the given base (usually 10) + mini_attr default={} SVG attributes for the minitick-lines + (if miniticks != False) + attribute=value pairs keyword list SVG attributes for the major tick lines + """ + defaults = {"stroke-width": "0.25pt", "stroke": "gray", } + mini_defaults = {"stroke-width": "0.25pt", "stroke": "lightgray", "stroke-dasharray": "1,1", } + + def __repr__(self): + return "" % ( + self.xmin, self.xmax, self.ymin, self.ymax, str(self.ticks), str(self.miniticks), self.attr) + + def __init__(self, xmin, xmax, ymin, ymax, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr): + self.xmin, self.xmax = xmin, xmax + self.ymin, self.ymax = ymin, ymax + + self.mini_attr = dict(self.mini_defaults) + self.mini_attr.update(mini_attr) + + Ticks.__init__(self, None, None, None, ticks, miniticks, None, logbase) + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + self.low, self.high = self.xmin, self.xmax + self.last_xticks, self.last_xminiticks = Ticks.interpret(self) + self.low, self.high = self.ymin, self.ymax + self.last_yticks, self.last_yminiticks = Ticks.interpret(self) + + ticksd = [] + for t in self.last_xticks.keys(): + ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d + for t in self.last_yticks.keys(): + ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d + + miniticksd = [] + for t in self.last_xminiticks: + miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d + for t in self.last_yminiticks: + miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d + + return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG()) + +###################################################################### + +class XErrorBars: + """Draws x error bars at a set of points. This is usually used + before (under) a set of Dots at the same points. + + XErrorBars(d, attribute=value) + + d required list of (x,y,xerr...) points + attribute=value pairs keyword list SVG attributes + + If points in d have + + * 3 elements, the third is the symmetric error bar + * 4 elements, the third and fourth are the asymmetric lower and + upper error bar. The third element should be negative, + e.g. (5, 5, -1, 2) is a bar from 4 to 7. + * more than 4, a tick mark is placed at each value. This lets + you nest errors from different sources, correlated and + uncorrelated, statistical and systematic, etc. + """ + defaults = {"stroke-width": "0.25pt", } + + def __repr__(self): + return "" % len(self.d) + + def __init__(self, d=[], **attr): + self.d = list(d) + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + if isinstance(trans, basestring): + trans = totrans(trans) # only once + + output = SVG("g") + for p in self.d: + x, y = p[0], p[1] + + if len(p) == 3: + bars = [x - p[2], x + p[2]] + else: + bars = [x + pi for pi in p[2:]] + + start, end = min(bars), max(bars) + output.append(LineAxis(start, y, end, y, start, end, bars, False, False, **self.attr).SVG(trans)) + + return output + + +class YErrorBars: + """Draws y error bars at a set of points. This is usually used + before (under) a set of Dots at the same points. + + YErrorBars(d, attribute=value) + + d required list of (x,y,yerr...) points + attribute=value pairs keyword list SVG attributes + + If points in d have + + * 3 elements, the third is the symmetric error bar + * 4 elements, the third and fourth are the asymmetric lower and + upper error bar. The third element should be negative, + e.g. (5, 5, -1, 2) is a bar from 4 to 7. + * more than 4, a tick mark is placed at each value. This lets + you nest errors from different sources, correlated and + uncorrelated, statistical and systematic, etc. + """ + defaults = {"stroke-width": "0.25pt", } + + def __repr__(self): + return "" % len(self.d) + + def __init__(self, d=[], **attr): + self.d = list(d) + + self.attr = dict(self.defaults) + self.attr.update(attr) + + def SVG(self, trans=None): + """Apply the transformation "trans" and return an SVG object.""" + if isinstance(trans, basestring): + trans = totrans(trans) # only once + + output = SVG("g") + for p in self.d: + x, y = p[0], p[1] + + if len(p) == 3: + bars = [y - p[2], y + p[2]] + else: + bars = [y + pi for pi in p[2:]] + + start, end = min(bars), max(bars) + output.append(LineAxis(x, start, x, end, start, end, bars, False, False, **self.attr).SVG(trans)) + + return output +