Reset platonic code.
[voro++.git] / branches / 2d_boundary / Tests / old / svgfig.py
blobaebc83e58cc6acfc8bab0c2da56b6cf9b3da40f5
1 # svgfig.py copyright (C) 2008 Jim Pivarski <jpivarski@gmail.com>
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
17 # Full licence is in the file COPYING and at http://www.gnu.org/copyleft/gpl.html
19 import re, codecs, os, platform, copy, itertools, math, cmath, random, sys, copy
20 _epsilon = 1e-5
23 if re.search("windows", platform.system(), re.I):
24 try:
25 import _winreg
26 _default_directory = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER, \
27 r"Software\Microsoft\Windows\Current Version\Explorer\Shell Folders"), "Desktop")[0]
28 # tmpdir = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Environment"), "TEMP")[0]
29 # if tmpdir[0:13] != "%USERPROFILE%":
30 # tmpdir = os.path.expanduser("~") + tmpdir[13:]
31 except:
32 _default_directory = os.path.expanduser("~") + os.sep + "Desktop"
34 _default_fileName = "tmp.svg"
36 _hacks = {}
37 _hacks["inkscape-text-vertical-shift"] = False
39 def rgb(r, g, b, maximum=1.):
40 """Create an SVG color string "#xxyyzz" from r, g, and b.
42 r,g,b = 0 is black and r,g,b = maximum is white.
43 """
44 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)))
46 def attr_preprocess(attr):
47 for name in attr.keys():
48 name_colon = re.sub("__", ":", name)
49 if name_colon != name:
50 attr[name_colon] = attr[name]
51 del attr[name]
52 name = name_colon
54 name_dash = re.sub("_", "-", name)
55 if name_dash != name:
56 attr[name_dash] = attr[name]
57 del attr[name]
58 name = name_dash
60 return attr
62 class SVG:
63 """A tree representation of an SVG image or image fragment.
65 SVG(t, sub, sub, sub..., attribute=value)
67 t required SVG type name
68 sub optional list nested SVG elements or text/Unicode
69 attribute=value pairs optional keywords SVG attributes
71 In attribute names, "__" becomes ":" and "_" becomes "-".
73 SVG in XML
75 <g id="mygroup" fill="blue">
76 <rect x="1" y="1" width="2" height="2" />
77 <rect x="3" y="3" width="2" height="2" />
78 </g>
80 SVG in Python
82 >>> svg = SVG("g", SVG("rect", x=1, y=1, width=2, height=2), \
83 ... SVG("rect", x=3, y=3, width=2, height=2), \
84 ... id="mygroup", fill="blue")
86 Sub-elements and attributes may be accessed through tree-indexing:
88 >>> svg = SVG("text", SVG("tspan", "hello there"), stroke="none", fill="black")
89 >>> svg[0]
90 <tspan (1 sub) />
91 >>> svg[0, 0]
92 'hello there'
93 >>> svg["fill"]
94 'black'
96 Iteration is depth-first:
98 >>> svg = SVG("g", SVG("g", SVG("line", x1=0, y1=0, x2=1, y2=1)), \
99 ... SVG("text", SVG("tspan", "hello again")))
100 ...
101 >>> for ti, s in svg:
102 ... print ti, repr(s)
103 ...
104 (0,) <g (1 sub) />
105 (0, 0) <line x2=1 y1=0 x1=0 y2=1 />
106 (0, 0, 'x2') 1
107 (0, 0, 'y1') 0
108 (0, 0, 'x1') 0
109 (0, 0, 'y2') 1
110 (1,) <text (1 sub) />
111 (1, 0) <tspan (1 sub) />
112 (1, 0, 0) 'hello again'
114 Use "print" to navigate:
116 >>> print svg
117 None <g (2 sub) />
118 [0] <g (1 sub) />
119 [0, 0] <line x2=1 y1=0 x1=0 y2=1 />
120 [1] <text (1 sub) />
121 [1, 0] <tspan (1 sub) />
123 def __init__(self, *t_sub, **attr):
124 if len(t_sub) == 0: raise TypeError, "SVG element must have a t (SVG type)"
126 # first argument is t (SVG type)
127 self.t = t_sub[0]
128 # the rest are sub-elements
129 self.sub = list(t_sub[1:])
131 # keyword arguments are attributes
132 # need to preprocess to handle differences between SVG and Python syntax
133 self.attr = attr_preprocess(attr)
135 def __getitem__(self, ti):
136 """Index is a list that descends tree, returning a sub-element if
137 it ends with a number and an attribute if it ends with a string."""
138 obj = self
139 if isinstance(ti, (list, tuple)):
140 for i in ti[:-1]: obj = obj[i]
141 ti = ti[-1]
143 if isinstance(ti, (int, long, slice)): return obj.sub[ti]
144 else: return obj.attr[ti]
146 def __setitem__(self, ti, value):
147 """Index is a list that descends tree, returning a sub-element if
148 it ends with a number and an attribute if it ends with a string."""
149 obj = self
150 if isinstance(ti, (list, tuple)):
151 for i in ti[:-1]: obj = obj[i]
152 ti = ti[-1]
154 if isinstance(ti, (int, long, slice)): obj.sub[ti] = value
155 else: obj.attr[ti] = value
157 def __delitem__(self, ti):
158 """Index is a list that descends tree, returning a sub-element if
159 it ends with a number and an attribute if it ends with a string."""
160 obj = self
161 if isinstance(ti, (list, tuple)):
162 for i in ti[:-1]: obj = obj[i]
163 ti = ti[-1]
165 if isinstance(ti, (int, long, slice)): del obj.sub[ti]
166 else: del obj.attr[ti]
168 def __contains__(self, value):
169 """x in svg == True iff x is an attribute in svg."""
170 return value in self.attr
172 def __eq__(self, other):
173 """x == y iff x represents the same SVG as y."""
174 if id(self) == id(other): return True
175 return isinstance(other, SVG) and self.t == other.t and self.sub == other.sub and self.attr == other.attr
177 def __ne__(self, other):
178 """x != y iff x does not represent the same SVG as y."""
179 return not (self == other)
181 def append(self, x):
182 """Appends x to the list of sub-elements (drawn last, overlaps
183 other primatives)."""
184 self.sub.append(x)
186 def prepend(self, x):
187 """Prepends x to the list of sub-elements (drawn first may be
188 overlapped by other primatives)."""
189 self.sub[0:0] = [x]
191 def extend(self, x):
192 """Extends list of sub-elements by a list x."""
193 self.sub.extend(x)
195 def clone(self, shallow=False):
196 """Deep copy of SVG tree. Set shallow=True for a shallow copy."""
197 if shallow:
198 return copy.copy(self)
199 else:
200 return copy.deepcopy(self)
202 ### nested class
203 class SVGDepthIterator:
204 """Manages SVG iteration."""
206 def __init__(self, svg, ti, depth_limit):
207 self.svg = svg
208 self.ti = ti
209 self.shown = False
210 self.depth_limit = depth_limit
212 def __iter__(self): return self
214 def next(self):
215 if not self.shown:
216 self.shown = True
217 if self.ti != ():
218 return self.ti, self.svg
220 if not isinstance(self.svg, SVG): raise StopIteration
221 if self.depth_limit != None and len(self.ti) >= self.depth_limit: raise StopIteration
223 if "iterators" not in self.__dict__:
224 self.iterators = []
225 for i, s in enumerate(self.svg.sub):
226 self.iterators.append(self.__class__(s, self.ti + (i,), self.depth_limit))
227 for k, s in self.svg.attr.items():
228 self.iterators.append(self.__class__(s, self.ti + (k,), self.depth_limit))
229 self.iterators = itertools.chain(*self.iterators)
231 return self.iterators.next()
232 ### end nested class
234 def depth_first(self, depth_limit=None):
235 """Returns a depth-first generator over the SVG. If depth_limit
236 is a number, stop recursion at that depth."""
237 return self.SVGDepthIterator(self, (), depth_limit)
239 def breadth_first(self, depth_limit=None):
240 """Not implemented yet. Any ideas on how to do it?
242 Returns a breadth-first generator over the SVG. If depth_limit
243 is a number, stop recursion at that depth."""
244 raise NotImplementedError, "Got an algorithm for breadth-first searching a tree without effectively copying the tree?"
246 def __iter__(self): return self.depth_first()
248 def items(self, sub=True, attr=True, text=True):
249 """Get a recursively-generated list of tree-index, sub-element/attribute pairs.
251 If sub == False, do not show sub-elements.
252 If attr == False, do not show attributes.
253 If text == False, do not show text/Unicode sub-elements.
255 output = []
256 for ti, s in self:
257 show = False
258 if isinstance(ti[-1], (int, long)):
259 if isinstance(s, basestring): show = text
260 else: show = sub
261 else: show = attr
263 if show: output.append((ti, s))
264 return output
266 def keys(self, sub=True, attr=True, text=True):
267 """Get a recursively-generated list of tree-indexes.
269 If sub == False, do not show sub-elements.
270 If attr == False, do not show attributes.
271 If text == False, do not show text/Unicode sub-elements.
273 return [ti for ti, s in self.items(sub, attr, text)]
275 def values(self, sub=True, attr=True, text=True):
276 """Get a recursively-generated list of sub-elements and attributes.
278 If sub == False, do not show sub-elements.
279 If attr == False, do not show attributes.
280 If text == False, do not show text/Unicode sub-elements.
282 return [s for ti, s in self.items(sub, attr, text)]
284 def __repr__(self): return self.xml(depth_limit=0)
286 def __str__(self):
287 """Print (actually, return a string of) the tree in a form useful for browsing."""
288 return self.tree(sub=True, attr=False, text=False)
290 def tree(self, depth_limit=None, sub=True, attr=True, text=True, tree_width=20, obj_width=80):
291 """Print (actually, return a string of) the tree in a form useful for browsing.
293 If depth_limit == a number, stop recursion at that depth.
294 If sub == False, do not show sub-elements.
295 If attr == False, do not show attributes.
296 If text == False, do not show text/Unicode sub-elements.
297 tree_width is the number of characters reserved for printing tree indexes.
298 obj_width is the number of characters reserved for printing sub-elements/attributes.
301 output = []
303 line = "%s %s" % (("%%-%ds" % tree_width) % repr(None), ("%%-%ds" % obj_width) % (repr(self))[0:obj_width])
304 output.append(line)
306 for ti, s in self.depth_first(depth_limit):
307 show = False
308 if isinstance(ti[-1], (int, long)):
309 if isinstance(s, basestring): show = text
310 else: show = sub
311 else: show = attr
313 if show:
314 line = "%s %s" % (("%%-%ds" % tree_width) % repr(list(ti)), ("%%-%ds" % obj_width) % (" "*len(ti) + repr(s))[0:obj_width])
315 output.append(line)
317 return "\n".join(output)
319 def xml(self, indent=" ", newl="\n", depth_limit=None, depth=0):
320 """Get an XML representation of the SVG.
322 indent string used for indenting
323 newl string used for newlines
324 If depth_limit == a number, stop recursion at that depth.
325 depth starting depth (not useful for users)
327 print svg.xml()
330 attrstr = []
331 for n, v in self.attr.items():
332 if isinstance(v, dict):
333 v = "; ".join(["%s:%s" % (ni, vi) for ni, vi in v.items()])
334 elif isinstance(v, (list, tuple)):
335 v = ", ".join(v)
336 attrstr.append(" %s=%s" % (n, repr(v)))
337 attrstr = "".join(attrstr)
339 if len(self.sub) == 0: return "%s<%s%s />" % (indent * depth, self.t, attrstr)
341 if depth_limit == None or depth_limit > depth:
342 substr = []
343 for s in self.sub:
344 if isinstance(s, SVG):
345 substr.append(s.xml(indent, newl, depth_limit, depth + 1) + newl)
346 elif isinstance(s, str):
347 substr.append("%s%s%s" % (indent * (depth + 1), s, newl))
348 else:
349 substr.append("%s%s%s" % (indent * (depth + 1), repr(s), newl))
350 substr = "".join(substr)
352 return "%s<%s%s>%s%s%s</%s>" % (indent * depth, self.t, attrstr, newl, substr, indent * depth, self.t)
354 else:
355 return "%s<%s (%d sub)%s />" % (indent * depth, self.t, len(self.sub), attrstr)
357 def standalone_xml(self, indent=" ", newl="\n"):
358 """Get an XML representation of the SVG that can be saved/rendered.
360 indent string used for indenting
361 newl string used for newlines
364 if self.t == "svg": top = self
365 else: top = canvas(self)
366 return """\
367 <?xml version="1.0" standalone="no"?>
368 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
370 """ + ("".join(top.__standalone_xml(indent, newl))) # end of return statement
372 def __standalone_xml(self, indent, newl):
373 output = [u"<%s" % self.t]
375 for n, v in self.attr.items():
376 if isinstance(v, dict):
377 v = "; ".join(["%s:%s" % (ni, vi) for ni, vi in v.items()])
378 elif isinstance(v, (list, tuple)):
379 v = ", ".join(v)
380 output.append(u" %s=\"%s\"" % (n, v))
382 if len(self.sub) == 0:
383 output.append(u" />%s%s" % (newl, newl))
384 return output
386 elif self.t == "text" or self.t == "tspan" or self.t == "style":
387 output.append(u">")
389 else:
390 output.append(u">%s%s" % (newl, newl))
392 for s in self.sub:
393 if isinstance(s, SVG): output.extend(s.__standalone_xml(indent, newl))
394 else: output.append(unicode(s))
396 if self.t == "tspan": output.append(u"</%s>" % self.t)
397 else: output.append(u"</%s>%s%s" % (self.t, newl, newl))
399 return output
401 def interpret_fileName(self, fileName=None):
402 if fileName == None:
403 fileName = _default_fileName
404 if re.search("windows", platform.system(), re.I) and not os.path.isabs(fileName):
405 fileName = _default_directory + os.sep + fileName
406 return fileName
408 def save(self, fileName=None, encoding="utf-8", compresslevel=None):
409 """Save to a file for viewing. Note that svg.save() overwrites the file named _default_fileName.
411 fileName default=None note that _default_fileName will be overwritten if
412 no fileName is specified. If the extension
413 is ".svgz" or ".gz", the output will be gzipped
414 encoding default="utf-8" file encoding (default is Unicode)
415 compresslevel default=None if a number, the output will be gzipped with that
416 compression level (1-9, 1 being fastest and 9 most
417 thorough)
419 fileName = self.interpret_fileName(fileName)
421 if compresslevel != None or re.search("\.svgz$", fileName, re.I) or re.search("\.gz$", fileName, re.I):
422 import gzip
423 if compresslevel == None:
424 f = gzip.GzipFile(fileName, "w")
425 else:
426 f = gzip.GzipFile(fileName, "w", compresslevel)
428 f = codecs.EncodedFile(f, "utf-8", encoding)
429 f.write(self.standalone_xml())
430 f.close()
432 else:
433 f = codecs.open(fileName, "w", encoding=encoding)
434 f.write(self.standalone_xml())
435 f.close()
437 def inkview(self, fileName=None, encoding="utf-8"):
438 """View in "inkview", assuming that program is available on your system.
440 fileName default=None note that any file named _default_fileName will be
441 overwritten if no fileName is specified. If the extension
442 is ".svgz" or ".gz", the output will be gzipped
443 encoding default="utf-8" file encoding (default is Unicode)
445 fileName = self.interpret_fileName(fileName)
446 self.save(fileName, encoding)
447 os.spawnvp(os.P_NOWAIT, "inkview", ("inkview", fileName))
449 def inkscape(self, fileName=None, encoding="utf-8"):
450 """View in "inkscape", assuming that program is available on your system.
452 fileName default=None note that any file named _default_fileName will be
453 overwritten if no fileName is specified. If the extension
454 is ".svgz" or ".gz", the output will be gzipped
455 encoding default="utf-8" file encoding (default is Unicode)
457 fileName = self.interpret_fileName(fileName)
458 self.save(fileName, encoding)
459 os.spawnvp(os.P_NOWAIT, "inkscape", ("inkscape", fileName))
461 def firefox(self, fileName=None, encoding="utf-8"):
462 """View in "firefox", assuming that program is available on your system.
464 fileName default=None note that any file named _default_fileName will be
465 overwritten if no fileName is specified. If the extension
466 is ".svgz" or ".gz", the output will be gzipped
467 encoding default="utf-8" file encoding (default is Unicode)
469 fileName = self.interpret_fileName(fileName)
470 self.save(fileName, encoding)
471 os.spawnvp(os.P_NOWAIT, "firefox", ("firefox", fileName))
473 ######################################################################
475 _canvas_defaults = {"width": "400px", "height": "400px", "viewBox": "0 0 100 100", \
476 "xmlns": "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", "version":"1.1", \
477 "style": {"stroke":"black", "fill":"none", "stroke-width":"0.5pt", "stroke-linejoin":"round", "text-anchor":"middle"}, \
478 "font-family": ["Helvetica", "Arial", "FreeSans", "Sans", "sans", "sans-serif"], \
481 def canvas(*sub, **attr):
482 """Creates a top-level SVG object, allowing the user to control the
483 image size and aspect ratio.
485 canvas(sub, sub, sub..., attribute=value)
487 sub optional list nested SVG elements or text/Unicode
488 attribute=value pairs optional keywords SVG attributes
490 Default attribute values:
492 width "400px"
493 height "400px"
494 viewBox "0 0 100 100"
495 xmlns "http://www.w3.org/2000/svg"
496 xmlns:xlink "http://www.w3.org/1999/xlink"
497 version "1.1"
498 style "stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoin:round; text-anchor:middle"
499 font-family "Helvetica,Arial,FreeSans?,Sans,sans,sans-serif"
501 attributes = dict(_canvas_defaults)
502 attributes.update(attr)
504 if sub == None or sub == ():
505 return SVG("svg", **attributes)
506 else:
507 return SVG("svg", *sub, **attributes)
509 def canvas_outline(*sub, **attr):
510 """Same as canvas(), but draws an outline around the drawable area,
511 so that you know how close your image is to the edges."""
512 svg = canvas(*sub, **attr)
513 match = re.match("[, \t]*([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]*", svg["viewBox"])
514 if match == None: raise ValueError, "canvas viewBox is incorrectly formatted"
515 x, y, width, height = [float(x) for x in match.groups()]
516 svg.prepend(SVG("rect", x=x, y=y, width=width, height=height, stroke="none", fill="cornsilk"))
517 svg.append(SVG("rect", x=x, y=y, width=width, height=height, stroke="black", fill="none"))
518 return svg
520 def template(fileName, svg, replaceme="REPLACEME"):
521 """Loads an SVG image from a file, replacing instances of
522 <REPLACEME /> with a given svg object.
524 fileName required name of the template SVG
525 svg required SVG object for replacement
526 replaceme default="REPLACEME" fake SVG element to be replaced by the given object
528 >>> print load("template.svg")
529 None <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
530 [0] <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
531 [1] <REPLACEME />
532 >>>
533 >>> print template("template.svg", SVG("circle", cx=50, cy=50, r=30))
534 None <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
535 [0] <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
536 [1] <circle cy=50 cx=50 r=30 />
538 output = load(fileName)
539 for ti, s in output:
540 if isinstance(s, SVG) and s.t == replaceme:
541 output[ti] = svg
542 return output
544 ######################################################################
546 def load(fileName):
547 """Loads an SVG image from a file."""
548 return load_stream(file(fileName))
550 def load_stream(stream):
551 """Loads an SVG image from a stream (can be a string or a file object)."""
553 from xml.sax import handler, make_parser
554 from xml.sax.handler import feature_namespaces, feature_external_ges, feature_external_pes
556 class ContentHandler(handler.ContentHandler):
557 def __init__(self):
558 self.stack = []
559 self.output = None
560 self.all_whitespace = re.compile("^\s*$")
562 def startElement(self, name, attr):
563 s = SVG(name)
564 s.attr = dict(attr.items())
565 if len(self.stack) > 0:
566 last = self.stack[-1]
567 last.sub.append(s)
568 self.stack.append(s)
570 def characters(self, ch):
571 if not isinstance(ch, basestring) or self.all_whitespace.match(ch) == None:
572 if len(self.stack) > 0:
573 last = self.stack[-1]
574 if len(last.sub) > 0 and isinstance(last.sub[-1], basestring):
575 last.sub[-1] = last.sub[-1] + "\n" + ch
576 else:
577 last.sub.append(ch)
579 def endElement(self, name):
580 if len(self.stack) > 0:
581 last = self.stack[-1]
582 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):
583 last.sub[0] = "<![CDATA[\n" + last.sub[0] + "]]>"
585 self.output = self.stack.pop()
587 ch = ContentHandler()
588 parser = make_parser()
589 parser.setContentHandler(ch)
590 parser.setFeature(feature_namespaces, 0)
591 parser.setFeature(feature_external_ges, 0)
592 parser.parse(stream)
593 return ch.output
595 ######################################################################
597 def totrans(expr, vars=("x", "y"), globals=None, locals=None):
598 """Converts to a coordinate transformation (a function that accepts
599 two arguments and returns two values).
601 expr required a string expression or a function
602 of two real or one complex value
603 vars default=("x", "y") independent variable names;
604 a singleton ("z",) is interpreted
605 as complex
606 globals default=None dict of global variables
607 locals default=None dict of local variables
610 if callable(expr):
611 if expr.func_code.co_argcount == 2:
612 return expr
614 elif expr.func_code.co_argcount == 1:
615 split = lambda z: (z.real, z.imag)
616 output = lambda x, y: split(expr(x + y*1j))
617 output.func_name = expr.func_name
618 return output
620 else:
621 raise TypeError, "must be a function of 2 or 1 variables"
623 if len(vars) == 2:
624 g = math.__dict__
625 if globals != None: g.update(globals)
626 output = eval("lambda %s, %s: (%s)" % (vars[0], vars[1], expr), g, locals)
627 output.func_name = "%s,%s -> %s" % (vars[0], vars[1], expr)
628 return output
630 elif len(vars) == 1:
631 g = cmath.__dict__
632 if globals != None: g.update(globals)
633 output = eval("lambda %s: (%s)" % (vars[0], expr), g, locals)
634 split = lambda z: (z.real, z.imag)
635 output2 = lambda x, y: split(output(x + y*1j))
636 output2.func_name = "%s -> %s" % (vars[0], expr)
637 return output2
639 else:
640 raise TypeError, "vars must have 2 or 1 elements"
642 def window(xmin, xmax, ymin, ymax, x=0, y=0, width=100, height=100, xlogbase=None, ylogbase=None, minusInfinity=-1000, flipx=False, flipy=True):
643 """Creates and returns a coordinate transformation (a function that
644 accepts two arguments and returns two values) that transforms from
645 (xmin, ymin), (xmax, ymax)
647 (x, y), (x + width, y + height).
649 xlogbase, ylogbase default=None, None if a number, transform
650 logarithmically with given base
651 minusInfinity default=-1000 what to return if
652 log(0 or negative) is attempted
653 flipx default=False if true, reverse the direction of x
654 flipy default=True if true, reverse the direction of y
656 (When composing windows, be sure to set flipy=False.)
659 if flipx:
660 ox1 = x + width
661 ox2 = x
662 else:
663 ox1 = x
664 ox2 = x + width
665 if flipy:
666 oy1 = y + height
667 oy2 = y
668 else:
669 oy1 = y
670 oy2 = y + height
671 ix1 = xmin
672 iy1 = ymin
673 ix2 = xmax
674 iy2 = ymax
676 if xlogbase != None and (ix1 <= 0. or ix2 <= 0.): raise ValueError, "x range incompatible with log scaling: (%g, %g)" % (ix1, ix2)
678 if ylogbase != None and (iy1 <= 0. or iy2 <= 0.): raise ValueError, "y range incompatible with log scaling: (%g, %g)" % (iy1, iy2)
680 def maybelog(t, it1, it2, ot1, ot2, logbase):
681 if t <= 0.: return minusInfinity
682 else:
683 return ot1 + 1.*(math.log(t, logbase) - math.log(it1, logbase))/(math.log(it2, logbase) - math.log(it1, logbase)) * (ot2 - ot1)
685 xlogstr, ylogstr = "", ""
687 if xlogbase == None:
688 xfunc = lambda x: ox1 + 1.*(x - ix1)/(ix2 - ix1) * (ox2 - ox1)
689 else:
690 xfunc = lambda x: maybelog(x, ix1, ix2, ox1, ox2, xlogbase)
691 xlogstr = " xlog=%g" % xlogbase
693 if ylogbase == None:
694 yfunc = lambda y: oy1 + 1.*(y - iy1)/(iy2 - iy1) * (oy2 - oy1)
695 else:
696 yfunc = lambda y: maybelog(y, ylogbase)
697 ylogstr = " ylog=%g" % ylogbase
699 output = lambda x, y: (xfunc(x), yfunc(y))
701 output.func_name = "(%g, %g), (%g, %g) -> (%g, %g), (%g, %g)%s%s" % (ix1, ix2, iy1, iy2, ox1, ox2, oy1, oy2, xlogstr, ylogstr)
702 return output
704 def rotate(angle, cx=0, cy=0):
705 """Creates and returns a coordinate transformation which rotates
706 around (cx,cy) by "angle" degrees."""
707 angle *= math.pi/180.
708 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))
710 class Fig:
711 """Stores graphics primitive objects and applies a single coordinate
712 transformation to them. To compose coordinate systems, nest Fig
713 objects.
715 Fig(obj, obj, obj..., trans=function)
717 obj optional list a list of drawing primatives
718 trans default=None a coordinate transformation function
720 >>> fig = Fig(Line(0,0,1,1), Rect(0.2,0.2,0.8,0.8), trans="2*x, 2*y")
721 >>> print fig.SVG().xml()
723 <path d='M0 0L2 2' />
724 <path d='M0.4 0.4L1.6 0.4ZL1.6 1.6ZL0.4 1.6ZL0.4 0.4ZZ' />
725 </g>
726 >>> print Fig(fig, trans="x/2., y/2.").SVG().xml()
728 <path d='M0 0L1 1' />
729 <path d='M0.2 0.2L0.8 0.2ZL0.8 0.8ZL0.2 0.8ZL0.2 0.2ZZ' />
730 </g>
733 def __repr__(self):
734 if self.trans == None:
735 return "<Fig (%d items)>" % len(self.d)
736 elif isinstance(self.trans, basestring):
737 return "<Fig (%d items) x,y -> %s>" % (len(self.d), self.trans)
738 else:
739 return "<Fig (%d items) %s>" % (len(self.d), self.trans.func_name)
741 def __init__(self, *d, **kwds):
742 self.d = list(d)
743 defaults = {"trans":None}
744 defaults.update(kwds)
745 kwds = defaults
747 self.trans = kwds["trans"]; del kwds["trans"]
748 if len(kwds) != 0:
749 raise TypeError, "Fig() got unexpected keyword arguments %s" % kwds.keys()
751 def SVG(self, trans=None):
752 """Apply the transformation "trans" and return an SVG object.
754 Coordinate transformations in nested Figs will be composed.
757 if trans == None: trans = self.trans
758 if isinstance(trans, basestring): trans = totrans(trans)
760 output = SVG("g")
761 for s in self.d:
762 if isinstance(s, SVG):
763 output.append(s)
765 elif isinstance(s, Fig):
766 strans = s.trans
767 if isinstance(strans, basestring): strans = totrans(strans)
769 if trans == None: subtrans = strans
770 elif strans == None: subtrans = trans
771 else: subtrans = lambda x,y: trans(*strans(x, y))
773 output.sub += s.SVG(subtrans).sub
775 elif s == None: pass
777 else:
778 output.append(s.SVG(trans))
780 return output
782 class Plot:
783 """Acts like Fig, but draws a coordinate axis. You also need to supply plot ranges.
785 Plot(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
787 xmin, xmax required minimum and maximum x values (in the objs' coordinates)
788 ymin, ymax required minimum and maximum y values (in the objs' coordinates)
789 obj optional list drawing primatives
790 keyword options keyword list options defined below
792 The following are keyword options, with their default values:
794 trans None transformation function
795 x, y 5, 5 upper-left corner of the Plot in SVG coordinates
796 width, height 90, 90 width and height of the Plot in SVG coordinates
797 flipx, flipy False, True flip the sign of the coordinate axis
798 minusInfinity -1000 if an axis is logarithmic and an object is plotted at 0 or
799 a negative value, -1000 will be used as a stand-in for NaN
800 atx, aty 0, 0 the place where the coordinate axes cross
801 xticks -10 request ticks according to the standard tick specification
802 (see help(Ticks))
803 xminiticks True request miniticks according to the standard minitick
804 specification
805 xlabels True request tick labels according to the standard tick label
806 specification
807 xlogbase None if a number, the axis and transformation are logarithmic
808 with ticks at the given base (10 being the most common)
809 (same for y)
810 arrows None if a new identifier, create arrow markers and draw them
811 at the ends of the coordinate axes
812 text_attr {} a dictionary of attributes for label text
813 axis_attr {} a dictionary of attributes for the axis lines
816 def __repr__(self):
817 if self.trans == None:
818 return "<Plot (%d items)>" % len(self.d)
819 else:
820 return "<Plot (%d items) %s>" % (len(self.d), self.trans.func_name)
822 def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
823 self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
824 self.d = list(d)
825 defaults = {"trans":None, "x":5, "y":5, "width":90, "height":90, "flipx":False, "flipy":True, "minusInfinity":-1000, \
826 "atx":0, "xticks":-10, "xminiticks":True, "xlabels":True, "xlogbase":None, \
827 "aty":0, "yticks":-10, "yminiticks":True, "ylabels":True, "ylogbase":None, \
828 "arrows":None, "text_attr":{}, "axis_attr":{}}
829 defaults.update(kwds)
830 kwds = defaults
832 self.trans = kwds["trans"]; del kwds["trans"]
833 self.x = kwds["x"]; del kwds["x"]
834 self.y = kwds["y"]; del kwds["y"]
835 self.width = kwds["width"]; del kwds["width"]
836 self.height = kwds["height"]; del kwds["height"]
837 self.flipx = kwds["flipx"]; del kwds["flipx"]
838 self.flipy = kwds["flipy"]; del kwds["flipy"]
839 self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
840 self.atx = kwds["atx"]; del kwds["atx"]
841 self.xticks = kwds["xticks"]; del kwds["xticks"]
842 self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
843 self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
844 self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
845 self.aty = kwds["aty"]; del kwds["aty"]
846 self.yticks = kwds["yticks"]; del kwds["yticks"]
847 self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
848 self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
849 self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
850 self.arrows = kwds["arrows"]; del kwds["arrows"]
851 self.text_attr = kwds["text_attr"]; del kwds["text_attr"]
852 self.axis_attr = kwds["axis_attr"]; del kwds["axis_attr"]
853 if len(kwds) != 0:
854 raise TypeError, "Plot() got unexpected keyword arguments %s" % kwds.keys()
856 def SVG(self, trans=None):
857 """Apply the transformation "trans" and return an SVG object."""
858 if trans == None: trans = self.trans
859 if isinstance(trans, basestring): trans = totrans(trans)
861 self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax, x=self.x, y=self.y, width=self.width, height=self.height, \
862 xlogbase=self.xlogbase, ylogbase=self.ylogbase, minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
864 d = [Axes(self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty, \
865 self.xticks, self.xminiticks, self.xlabels, self.xlogbase, \
866 self.yticks, self.yminiticks, self.ylabels, self.ylogbase, \
867 self.arrows, self.text_attr, **self.axis_attr)] \
868 + self.d
870 return Fig(Fig(*d, **{"trans":trans})).SVG(self.last_window)
872 class Frame:
873 text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
874 axis_defaults = {}
876 tick_length = 1.5
877 minitick_length = 0.75
878 text_xaxis_offset = 1.
879 text_yaxis_offset = 2.
880 text_xtitle_offset = 6.
881 text_ytitle_offset = 12.
883 def __repr__(self):
884 return "<Frame (%d items)>" % len(self.d)
886 def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
887 """Acts like Fig, but draws a coordinate frame around the data. You also need to supply plot ranges.
889 Frame(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
891 xmin, xmax required minimum and maximum x values (in the objs' coordinates)
892 ymin, ymax required minimum and maximum y values (in the objs' coordinates)
893 obj optional list drawing primatives
894 keyword options keyword list options defined below
896 The following are keyword options, with their default values:
898 x, y 20, 5 upper-left corner of the Frame in SVG coordinates
899 width, height 75, 80 width and height of the Frame in SVG coordinates
900 flipx, flipy False, True flip the sign of the coordinate axis
901 minusInfinity -1000 if an axis is logarithmic and an object is plotted at 0 or
902 a negative value, -1000 will be used as a stand-in for NaN
903 xtitle None if a string, label the x axis
904 xticks -10 request ticks according to the standard tick specification
905 (see help(Ticks))
906 xminiticks True request miniticks according to the standard minitick
907 specification
908 xlabels True request tick labels according to the standard tick label
909 specification
910 xlogbase None if a number, the axis and transformation are logarithmic
911 with ticks at the given base (10 being the most common)
912 (same for y)
913 text_attr {} a dictionary of attributes for label text
914 axis_attr {} a dictionary of attributes for the axis lines
917 self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
918 self.d = list(d)
919 defaults = {"x":20, "y":5, "width":75, "height":80, "flipx":False, "flipy":True, "minusInfinity":-1000, \
920 "xtitle":None, "xticks":-10, "xminiticks":True, "xlabels":True, "x2labels":None, "xlogbase":None, \
921 "ytitle":None, "yticks":-10, "yminiticks":True, "ylabels":True, "y2labels":None, "ylogbase":None, \
922 "text_attr":{}, "axis_attr":{}}
923 defaults.update(kwds)
924 kwds = defaults
926 self.x = kwds["x"]; del kwds["x"]
927 self.y = kwds["y"]; del kwds["y"]
928 self.width = kwds["width"]; del kwds["width"]
929 self.height = kwds["height"]; del kwds["height"]
930 self.flipx = kwds["flipx"]; del kwds["flipx"]
931 self.flipy = kwds["flipy"]; del kwds["flipy"]
932 self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
933 self.xtitle = kwds["xtitle"]; del kwds["xtitle"]
934 self.xticks = kwds["xticks"]; del kwds["xticks"]
935 self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
936 self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
937 self.x2labels = kwds["x2labels"]; del kwds["x2labels"]
938 self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
939 self.ytitle = kwds["ytitle"]; del kwds["ytitle"]
940 self.yticks = kwds["yticks"]; del kwds["yticks"]
941 self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
942 self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
943 self.y2labels = kwds["y2labels"]; del kwds["y2labels"]
944 self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
946 self.text_attr = dict(self.text_defaults)
947 self.text_attr.update(kwds["text_attr"]); del kwds["text_attr"]
949 self.axis_attr = dict(self.axis_defaults)
950 self.axis_attr.update(kwds["axis_attr"]); del kwds["axis_attr"]
952 if len(kwds) != 0:
953 raise TypeError, "Frame() got unexpected keyword arguments %s" % kwds.keys()
955 def SVG(self):
956 """Apply the window transformation and return an SVG object."""
958 self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax, x=self.x, y=self.y, width=self.width, height=self.height, \
959 xlogbase=self.xlogbase, ylogbase=self.ylogbase, minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
961 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)
962 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)
963 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)
964 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)
966 left.tick_start = -self.tick_length
967 left.tick_end = 0
968 left.minitick_start = -self.minitick_length
969 left.minitick_end = 0.
970 left.text_start = self.text_yaxis_offset
972 right.tick_start = 0.
973 right.tick_end = self.tick_length
974 right.minitick_start = 0.
975 right.minitick_end = self.minitick_length
976 right.text_start = -self.text_yaxis_offset
977 right.text_attr["text-anchor"] = "start"
979 bottom.tick_start = 0.
980 bottom.tick_end = self.tick_length
981 bottom.minitick_start = 0.
982 bottom.minitick_end = self.minitick_length
983 bottom.text_start = -self.text_xaxis_offset
985 top.tick_start = -self.tick_length
986 top.tick_end = 0.
987 top.minitick_start = -self.minitick_length
988 top.minitick_end = 0.
989 top.text_start = self.text_xaxis_offset
990 top.text_attr["dominant-baseline"] = "text-after-edge"
992 output = Fig(*self.d).SVG(self.last_window)
993 output.prepend(left.SVG(self.last_window))
994 output.prepend(bottom.SVG(self.last_window))
995 output.prepend(right.SVG(self.last_window))
996 output.prepend(top.SVG(self.last_window))
998 if self.xtitle != None:
999 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))
1000 if self.ytitle != None:
1001 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))
1002 return output
1004 ######################################################################
1006 def pathtoPath(svg):
1007 """Converts SVG("path", d="...") into Path(d=[...])."""
1008 if not isinstance(svg, SVG) or svg.t != "path":
1009 raise TypeError, "Only SVG <path /> objects can be converted into Paths"
1010 attr = dict(svg.attr)
1011 d = attr["d"]
1012 del attr["d"]
1013 for key in attr.keys():
1014 if not isinstance(key, str):
1015 value = attr[key]
1016 del attr[key]
1017 attr[str(key)] = value
1018 return Path(d, **attr)
1020 class Path:
1021 """Path represents an SVG path, an arbitrary set of curves and
1022 straight segments. Unlike SVG("path", d="..."), Path stores
1023 coordinates as a list of numbers, rather than a string, so that it is
1024 transformable in a Fig.
1026 Path(d, attribute=value)
1028 d required path data
1029 attribute=value pairs keyword list SVG attributes
1031 See http://www.w3.org/TR/SVG/paths.html for specification of paths
1032 from text.
1034 Internally, Path data is a list of tuples with these definitions:
1036 * ("Z/z",): close the current path
1037 * ("H/h", x) or ("V/v", y): a horizontal or vertical line
1038 segment to x or y
1039 * ("M/m/L/l/T/t", x, y, global): moveto, lineto, or smooth
1040 quadratic curveto point (x, y). If global=True, (x, y) should
1041 not be transformed.
1042 * ("S/sQ/q", cx, cy, cglobal, x, y, global): polybezier or
1043 smooth quadratic curveto point (x, y) using (cx, cy) as a
1044 control point. If cglobal or global=True, (cx, cy) or (x, y)
1045 should not be transformed.
1046 * ("C/c", c1x, c1y, c1global, c2x, c2y, c2global, x, y, global):
1047 cubic curveto point (x, y) using (c1x, c1y) and (c2x, c2y) as
1048 control points. If c1global, c2global, or global=True, (c1x, c1y),
1049 (c2x, c2y), or (x, y) should not be transformed.
1050 * ("A/a", rx, ry, rglobal, x-axis-rotation, angle, large-arc-flag,
1051 sweep-flag, x, y, global): arcto point (x, y) using the
1052 aforementioned parameters.
1053 * (",/.", rx, ry, rglobal, angle, x, y, global): an ellipse at
1054 point (x, y) with radii (rx, ry). If angle is 0, the whole
1055 ellipse is drawn; otherwise, a partial ellipse is drawn.
1057 defaults = {}
1059 def __repr__(self):
1060 return "<Path (%d nodes) %s>" % (len(self.d), self.attr)
1062 def __init__(self, d=[], **attr):
1063 if isinstance(d, basestring): self.d = self.parse(d)
1064 else: self.d = list(d)
1066 self.attr = dict(self.defaults)
1067 self.attr.update(attr)
1069 def parse_whitespace(self, index, pathdata):
1070 """Part of Path's text-command parsing algorithm; used internally."""
1071 while index < len(pathdata) and pathdata[index] in (" ", "\t", "\r", "\n", ","): index += 1
1072 return index, pathdata
1074 def parse_command(self, index, pathdata):
1075 """Part of Path's text-command parsing algorithm; used internally."""
1076 index, pathdata = self.parse_whitespace(index, pathdata)
1078 if index >= len(pathdata): return None, index, pathdata
1079 command = pathdata[index]
1080 if "A" <= command <= "Z" or "a" <= command <= "z":
1081 index += 1
1082 return command, index, pathdata
1083 else:
1084 return None, index, pathdata
1086 def parse_number(self, index, pathdata):
1087 """Part of Path's text-command parsing algorithm; used internally."""
1088 index, pathdata = self.parse_whitespace(index, pathdata)
1090 if index >= len(pathdata): return None, index, pathdata
1091 first_digit = pathdata[index]
1093 if "0" <= first_digit <= "9" or first_digit in ("-", "+", "."):
1094 start = index
1095 while index < len(pathdata) and ("0" <= pathdata[index] <= "9" or pathdata[index] in ("-", "+", ".", "e", "E")):
1096 index += 1
1097 end = index
1099 index = end
1100 return float(pathdata[start:end]), index, pathdata
1101 else:
1102 return None, index, pathdata
1104 def parse_boolean(self, index, pathdata):
1105 """Part of Path's text-command parsing algorithm; used internally."""
1106 index, pathdata = self.parse_whitespace(index, pathdata)
1108 if index >= len(pathdata): return None, index, pathdata
1109 first_digit = pathdata[index]
1111 if first_digit in ("0", "1"):
1112 index += 1
1113 return int(first_digit), index, pathdata
1114 else:
1115 return None, index, pathdata
1117 def parse(self, pathdata):
1118 """Parses text-commands, converting them into a list of tuples.
1119 Called by the constructor."""
1120 output = []
1121 index = 0
1122 while True:
1123 command, index, pathdata = self.parse_command(index, pathdata)
1124 index, pathdata = self.parse_whitespace(index, pathdata)
1126 if command == None and index == len(pathdata): break # this is the normal way out of the loop
1127 if command in ("Z", "z"):
1128 output.append((command,))
1130 ######################
1131 elif command in ("H", "h", "V", "v"):
1132 errstring = "Path command \"%s\" requires a number at index %d" % (command, index)
1133 num1, index, pathdata = self.parse_number(index, pathdata)
1134 if num1 == None: raise ValueError, errstring
1136 while num1 != None:
1137 output.append((command, num1))
1138 num1, index, pathdata = self.parse_number(index, pathdata)
1140 ######################
1141 elif command in ("M", "m", "L", "l", "T", "t"):
1142 errstring = "Path command \"%s\" requires an x,y pair at index %d" % (command, index)
1143 num1, index, pathdata = self.parse_number(index, pathdata)
1144 num2, index, pathdata = self.parse_number(index, pathdata)
1146 if num1 == None: raise ValueError, errstring
1148 while num1 != None:
1149 if num2 == None: raise ValueError, errstring
1150 output.append((command, num1, num2, False))
1152 num1, index, pathdata = self.parse_number(index, pathdata)
1153 num2, index, pathdata = self.parse_number(index, pathdata)
1155 ######################
1156 elif command in ("S", "s", "Q", "q"):
1157 errstring = "Path command \"%s\" requires a cx,cy,x,y quadruplet at index %d" % (command, index)
1158 num1, index, pathdata = self.parse_number(index, pathdata)
1159 num2, index, pathdata = self.parse_number(index, pathdata)
1160 num3, index, pathdata = self.parse_number(index, pathdata)
1161 num4, index, pathdata = self.parse_number(index, pathdata)
1163 if num1 == None: raise ValueError, errstring
1165 while num1 != None:
1166 if num2 == None or num3 == None or num4 == None: raise ValueError, errstring
1167 output.append((command, num1, num2, False, num3, num4, False))
1169 num1, index, pathdata = self.parse_number(index, pathdata)
1170 num2, index, pathdata = self.parse_number(index, pathdata)
1171 num3, index, pathdata = self.parse_number(index, pathdata)
1172 num4, index, pathdata = self.parse_number(index, pathdata)
1174 ######################
1175 elif command in ("C", "c"):
1176 errstring = "Path command \"%s\" requires a c1x,c1y,c2x,c2y,x,y sextuplet at index %d" % (command, index)
1177 num1, index, pathdata = self.parse_number(index, pathdata)
1178 num2, index, pathdata = self.parse_number(index, pathdata)
1179 num3, index, pathdata = self.parse_number(index, pathdata)
1180 num4, index, pathdata = self.parse_number(index, pathdata)
1181 num5, index, pathdata = self.parse_number(index, pathdata)
1182 num6, index, pathdata = self.parse_number(index, pathdata)
1184 if num1 == None: raise ValueError, errstring
1186 while num1 != None:
1187 if num2 == None or num3 == None or num4 == None or num5 == None or num6 == None: raise ValueError, errstring
1189 output.append((command, num1, num2, False, num3, num4, False, num5, num6, False))
1191 num1, index, pathdata = self.parse_number(index, pathdata)
1192 num2, index, pathdata = self.parse_number(index, pathdata)
1193 num3, index, pathdata = self.parse_number(index, pathdata)
1194 num4, index, pathdata = self.parse_number(index, pathdata)
1195 num5, index, pathdata = self.parse_number(index, pathdata)
1196 num6, index, pathdata = self.parse_number(index, pathdata)
1198 ######################
1199 elif command in ("A", "a"):
1200 errstring = "Path command \"%s\" requires a rx,ry,angle,large-arc-flag,sweep-flag,x,y septuplet at index %d" % (command, index)
1201 num1, index, pathdata = self.parse_number(index, pathdata)
1202 num2, index, pathdata = self.parse_number(index, pathdata)
1203 num3, index, pathdata = self.parse_number(index, pathdata)
1204 num4, index, pathdata = self.parse_boolean(index, pathdata)
1205 num5, index, pathdata = self.parse_boolean(index, pathdata)
1206 num6, index, pathdata = self.parse_number(index, pathdata)
1207 num7, index, pathdata = self.parse_number(index, pathdata)
1209 if num1 == None: raise ValueError, errstring
1211 while num1 != None:
1212 if num2 == None or num3 == None or num4 == None or num5 == None or num6 == None or num7 == None: raise ValueError, errstring
1214 output.append((command, num1, num2, False, num3, num4, num5, num6, num7, False))
1216 num1, index, pathdata = self.parse_number(index, pathdata)
1217 num2, index, pathdata = self.parse_number(index, pathdata)
1218 num3, index, pathdata = self.parse_number(index, pathdata)
1219 num4, index, pathdata = self.parse_boolean(index, pathdata)
1220 num5, index, pathdata = self.parse_boolean(index, pathdata)
1221 num6, index, pathdata = self.parse_number(index, pathdata)
1222 num7, index, pathdata = self.parse_number(index, pathdata)
1224 return output
1226 def SVG(self, trans=None):
1227 """Apply the transformation "trans" and return an SVG object."""
1228 if isinstance(trans, basestring): trans = totrans(trans)
1230 x, y, X, Y = None, None, None, None
1231 output = []
1232 for datum in self.d:
1233 if not isinstance(datum, (tuple, list)):
1234 raise TypeError, "pathdata elements must be tuples/lists"
1236 command = datum[0]
1238 ######################
1239 if command in ("Z", "z"):
1240 x, y, X, Y = None, None, None, None
1241 output.append("Z")
1243 ######################
1244 elif command in ("H", "h", "V", "v"):
1245 command, num1 = datum
1247 if command == "H" or (command == "h" and x == None): x = num1
1248 elif command == "h": x += num1
1249 elif command == "V" or (command == "v" and y == None): y = num1
1250 elif command == "v": y += num1
1252 if trans == None: X, Y = x, y
1253 else: X, Y = trans(x, y)
1255 output.append("L%g %g" % (X, Y))
1257 ######################
1258 elif command in ("M", "m", "L", "l", "T", "t"):
1259 command, num1, num2, isglobal12 = datum
1261 if trans == None or isglobal12:
1262 if command.isupper() or X == None or Y == None:
1263 X, Y = num1, num2
1264 else:
1265 X += num1
1266 Y += num2
1267 x, y = X, Y
1269 else:
1270 if command.isupper() or x == None or y == None:
1271 x, y = num1, num2
1272 else:
1273 x += num1
1274 y += num2
1275 X, Y = trans(x, y)
1277 COMMAND = command.capitalize()
1278 output.append("%s%g %g" % (COMMAND, X, Y))
1280 ######################
1281 elif command in ("S", "s", "Q", "q"):
1282 command, num1, num2, isglobal12, num3, num4, isglobal34 = datum
1284 if trans == None or isglobal12:
1285 if command.isupper() or X == None or Y == None:
1286 CX, CY = num1, num2
1287 else:
1288 CX = X + num1
1289 CY = Y + num2
1291 else:
1292 if command.isupper() or x == None or y == None:
1293 cx, cy = num1, num2
1294 else:
1295 cx = x + num1
1296 cy = y + num2
1297 CX, CY = trans(cx, cy)
1299 if trans == None or isglobal34:
1300 if command.isupper() or X == None or Y == None:
1301 X, Y = num3, num4
1302 else:
1303 X += num3
1304 Y += num4
1305 x, y = X, Y
1307 else:
1308 if command.isupper() or x == None or y == None:
1309 x, y = num3, num4
1310 else:
1311 x += num3
1312 y += num4
1313 X, Y = trans(x, y)
1315 COMMAND = command.capitalize()
1316 output.append("%s%g %g %g %g" % (COMMAND, CX, CY, X, Y))
1318 ######################
1319 elif command in ("C", "c"):
1320 command, num1, num2, isglobal12, num3, num4, isglobal34, num5, num6, isglobal56 = datum
1322 if trans == None or isglobal12:
1323 if command.isupper() or X == None or Y == None:
1324 C1X, C1Y = num1, num2
1325 else:
1326 C1X = X + num1
1327 C1Y = Y + num2
1329 else:
1330 if command.isupper() or x == None or y == None:
1331 c1x, c1y = num1, num2
1332 else:
1333 c1x = x + num1
1334 c1y = y + num2
1335 C1X, C1Y = trans(c1x, c1y)
1337 if trans == None or isglobal34:
1338 if command.isupper() or X == None or Y == None:
1339 C2X, C2Y = num3, num4
1340 else:
1341 C2X = X + num3
1342 C2Y = Y + num4
1344 else:
1345 if command.isupper() or x == None or y == None:
1346 c2x, c2y = num3, num4
1347 else:
1348 c2x = x + num3
1349 c2y = y + num4
1350 C2X, C2Y = trans(c2x, c2y)
1352 if trans == None or isglobal56:
1353 if command.isupper() or X == None or Y == None:
1354 X, Y = num5, num6
1355 else:
1356 X += num5
1357 Y += num6
1358 x, y = X, Y
1360 else:
1361 if command.isupper() or x == None or y == None:
1362 x, y = num5, num6
1363 else:
1364 x += num5
1365 y += num6
1366 X, Y = trans(x, y)
1368 COMMAND = command.capitalize()
1369 output.append("%s%g %g %g %g %g %g" % (COMMAND, C1X, C1Y, C2X, C2Y, X, Y))
1371 ######################
1372 elif command in ("A", "a"):
1373 command, num1, num2, isglobal12, angle, large_arc_flag, sweep_flag, num3, num4, isglobal34 = datum
1375 oldx, oldy = x, y
1376 OLDX, OLDY = X, Y
1378 if trans == None or isglobal34:
1379 if command.isupper() or X == None or Y == None:
1380 X, Y = num3, num4
1381 else:
1382 X += num3
1383 Y += num4
1384 x, y = X, Y
1386 else:
1387 if command.isupper() or x == None or y == None:
1388 x, y = num3, num4
1389 else:
1390 x += num3
1391 y += num4
1392 X, Y = trans(x, y)
1394 if x != None and y != None:
1395 centerx, centery = (x + oldx)/2., (y + oldy)/2.
1396 CENTERX, CENTERY = (X + OLDX)/2., (Y + OLDY)/2.
1398 if trans == None or isglobal12:
1399 RX = CENTERX + num1
1400 RY = CENTERY + num2
1402 else:
1403 rx = centerx + num1
1404 ry = centery + num2
1405 RX, RY = trans(rx, ry)
1407 COMMAND = command.capitalize()
1408 output.append("%s%g %g %g %d %d %g %g" % (COMMAND, RX - CENTERX, RY - CENTERY, angle, large_arc_flag, sweep_flag, X, Y))
1410 elif command in (",", "."):
1411 command, num1, num2, isglobal12, angle, num3, num4, isglobal34 = datum
1412 if trans == None or isglobal34:
1413 if command == "." or X == None or Y == None:
1414 X, Y = num3, num4
1415 else:
1416 X += num3
1417 Y += num4
1418 x, y = None, None
1420 else:
1421 if command == "." or x == None or y == None:
1422 x, y = num3, num4
1423 else:
1424 x += num3
1425 y += num4
1426 X, Y = trans(x, y)
1428 if trans == None or isglobal12:
1429 RX = X + num1
1430 RY = Y + num2
1432 else:
1433 rx = x + num1
1434 ry = y + num2
1435 RX, RY = trans(rx, ry)
1437 RX, RY = RX - X, RY - Y
1439 X1, Y1 = X + RX * math.cos(angle*math.pi/180.), Y + RX * math.sin(angle*math.pi/180.)
1440 X2, Y2 = X + RY * math.sin(angle*math.pi/180.), Y - RY * math.cos(angle*math.pi/180.)
1441 X3, Y3 = X - RX * math.cos(angle*math.pi/180.), Y - RX * math.sin(angle*math.pi/180.)
1442 X4, Y4 = X - RY * math.sin(angle*math.pi/180.), Y + RY * math.cos(angle*math.pi/180.)
1444 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" \
1445 % (X1, Y1, RX, RY, angle, X2, Y2, RX, RY, angle, X3, Y3, RX, RY, angle, X4, Y4, RX, RY, angle, X1, Y1))
1447 return SVG("path", d="".join(output), **self.attr)
1449 ######################################################################
1451 def funcRtoC(expr, var="t", globals=None, locals=None):
1452 """Converts a complex "z(t)" string to a function acceptable for Curve.
1454 expr required string in the form "z(t)"
1455 var default="t" name of the independent variable
1456 globals default=None dict of global variables used in the expression;
1457 you may want to use Python's builtin globals()
1458 locals default=None dict of local variables
1460 g = cmath.__dict__
1461 if globals != None: g.update(globals)
1462 output = eval("lambda %s: (%s)" % (var, expr), g, locals)
1463 split = lambda z: (z.real, z.imag)
1464 output2 = lambda t: split(output(t))
1465 output2.func_name = "%s -> %s" % (var, expr)
1466 return output2
1468 def funcRtoR2(expr, var="t", globals=None, locals=None):
1469 """Converts a "f(t), g(t)" string to a function acceptable for Curve.
1471 expr required string in the form "f(t), g(t)"
1472 var default="t" name of the independent variable
1473 globals default=None dict of global variables used in the expression;
1474 you may want to use Python's builtin globals()
1475 locals default=None dict of local variables
1477 g = math.__dict__
1478 if globals != None: g.update(globals)
1479 output = eval("lambda %s: (%s)" % (var, expr), g, locals)
1480 output.func_name = "%s -> %s" % (var, expr)
1481 return output
1483 def funcRtoR(expr, var="x", globals=None, locals=None):
1484 """Converts a "f(x)" string to a function acceptable for Curve.
1486 expr required string in the form "f(x)"
1487 var default="x" name of the independent variable
1488 globals default=None dict of global variables used in the expression;
1489 you may want to use Python's builtin globals()
1490 locals default=None dict of local variables
1492 g = math.__dict__
1493 if globals != None: g.update(globals)
1494 output = eval("lambda %s: (%s, %s)" % (var, var, expr), g, locals)
1495 output.func_name = "%s -> %s" % (var, expr)
1496 return output
1498 class Curve:
1499 """Draws a parametric function as a path.
1501 Curve(f, low, high, loop, attribute=value)
1503 f required a Python callable or string in
1504 the form "f(t), g(t)"
1505 low, high required left and right endpoints
1506 loop default=False if True, connect the endpoints
1507 attribute=value pairs keyword list SVG attributes
1509 defaults = {}
1510 random_sampling = True
1511 recursion_limit = 15
1512 linearity_limit = 0.05
1513 discontinuity_limit = 5.
1515 def __repr__(self):
1516 return "<Curve %s [%s, %s] %s>" % (self.f, self.low, self.high, self.attr)
1518 def __init__(self, f, low, high, loop=False, **attr):
1519 self.f = f
1520 self.low = low
1521 self.high = high
1522 self.loop = loop
1524 self.attr = dict(self.defaults)
1525 self.attr.update(attr)
1527 ### nested class Sample
1528 class Sample:
1529 def __repr__(self):
1530 t, x, y, X, Y = self.t, self.x, self.y, self.X, self.Y
1531 if t != None: t = "%g" % t
1532 if x != None: x = "%g" % x
1533 if y != None: y = "%g" % y
1534 if X != None: X = "%g" % X
1535 if Y != None: Y = "%g" % Y
1536 return "<Curve.Sample t=%s x=%s y=%s X=%s Y=%s>" % (t, x, y, X, Y)
1538 def __init__(self, t): self.t = t
1540 def link(self, left, right): self.left, self.right = left, right
1542 def evaluate(self, f, trans):
1543 self.x, self.y = f(self.t)
1544 if trans == None:
1545 self.X, self.Y = self.x, self.y
1546 else:
1547 self.X, self.Y = trans(self.x, self.y)
1548 ### end Sample
1550 ### nested class Samples
1551 class Samples:
1552 def __repr__(self): return "<Curve.Samples (%d samples)>" % len(self)
1554 def __init__(self, left, right): self.left, self.right = left, right
1556 def __len__(self):
1557 count = 0
1558 current = self.left
1559 while current != None:
1560 count += 1
1561 current = current.right
1562 return count
1564 def __iter__(self):
1565 self.current = self.left
1566 return self
1568 def next(self):
1569 current = self.current
1570 if current == None: raise StopIteration
1571 self.current = self.current.right
1572 return current
1573 ### end nested class
1575 def sample(self, trans=None):
1576 """Adaptive-sampling algorithm that chooses the best sample points
1577 for a parametric curve between two endpoints and detects
1578 discontinuities. Called by SVG()."""
1579 oldrecursionlimit = sys.getrecursionlimit()
1580 sys.setrecursionlimit(self.recursion_limit + 100)
1581 try:
1582 # the best way to keep all the information while sampling is to make a linked list
1583 if not (self.low < self.high): raise ValueError, "low must be less than high"
1584 low, high = self.Sample(float(self.low)), self.Sample(float(self.high))
1585 low.link(None, high)
1586 high.link(low, None)
1588 low.evaluate(self.f, trans)
1589 high.evaluate(self.f, trans)
1591 # adaptive sampling between the low and high points
1592 self.subsample(low, high, 0, trans)
1594 # Prune excess points where the curve is nearly linear
1595 left = low
1596 while left.right != None:
1597 # increment mid and right
1598 mid = left.right
1599 right = mid.right
1600 if right != None and left.X != None and left.Y != None and mid.X != None and mid.Y != None and right.X != None and right.Y != None:
1601 numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
1602 denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
1603 if denom != 0. and abs(numer/denom) < self.linearity_limit:
1604 # drop mid (the garbage collector will get it)
1605 left.right = right
1606 right.left = left
1607 else:
1608 # increment left
1609 left = left.right
1610 else:
1611 left = left.right
1613 self.last_samples = self.Samples(low, high)
1615 finally:
1616 sys.setrecursionlimit(oldrecursionlimit)
1618 def subsample(self, left, right, depth, trans=None):
1619 """Part of the adaptive-sampling algorithm that chooses the best
1620 sample points. Called by sample()."""
1622 if self.random_sampling:
1623 mid = self.Sample(left.t + random.uniform(0.3, 0.7) * (right.t - left.t))
1624 else:
1625 mid = self.Sample(left.t + 0.5 * (right.t - left.t))
1627 left.right = mid
1628 right.left = mid
1629 mid.link(left, right)
1630 mid.evaluate(self.f, trans)
1632 # calculate the distance of closest approach of mid to the line between left and right
1633 numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
1634 denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
1636 # if we haven't sampled enough or left fails to be close enough to right, or mid fails to be linear enough...
1637 if depth < 3 or (denom == 0 and left.t != right.t) or denom > self.discontinuity_limit or (denom != 0. and abs(numer/denom) > self.linearity_limit):
1639 # and we haven't sampled too many points
1640 if depth < self.recursion_limit:
1641 self.subsample(left, mid, depth+1, trans)
1642 self.subsample(mid, right, depth+1, trans)
1644 else:
1645 # We've sampled many points and yet it's still not a small linear gap.
1646 # Break the line: it's a discontinuity
1647 mid.y = mid.Y = None
1649 def SVG(self, trans=None):
1650 """Apply the transformation "trans" and return an SVG object."""
1651 return self.Path(trans).SVG()
1653 def Path(self, trans=None, local=False):
1654 """Apply the transformation "trans" and return a Path object in
1655 global coordinates. If local=True, return a Path in local coordinates
1656 (which must be transformed again)."""
1658 if isinstance(trans, basestring): trans = totrans(trans)
1659 if isinstance(self.f, basestring): self.f = funcRtoR2(self.f)
1661 self.sample(trans)
1663 output = []
1664 for s in self.last_samples:
1665 if s.X != None and s.Y != None:
1666 if s.left == None or s.left.Y == None:
1667 command = "M"
1668 else:
1669 command = "L"
1671 if local: output.append((command, s.x, s.y, False))
1672 else: output.append((command, s.X, s.Y, True))
1674 if self.loop: output.append(("Z",))
1675 return Path(output, **self.attr)
1677 ######################################################################
1679 class Poly:
1680 """Draws a curve specified by a sequence of points. The curve may be
1681 piecewise linear, like a polygon, or a Bezier curve.
1683 Poly(d, mode, loop, attribute=value)
1685 d required list of tuples representing points
1686 and possibly control points
1687 mode default="L" "lines", "bezier", "velocity",
1688 "foreback", "smooth", or an abbreviation
1689 loop default=False if True, connect the first and last
1690 point, closing the loop
1691 attribute=value pairs keyword list SVG attributes
1693 The format of the tuples in d depends on the mode.
1695 "lines"/"L" d=[(x,y), (x,y), ...]
1696 piecewise-linear segments joining the (x,y) points
1697 "bezier"/"B" d=[(x, y, c1x, c1y, c2x, c2y), ...]
1698 Bezier curve with two control points (control points
1699 preceed (x,y), as in SVG paths). If (c1x,c1y) and
1700 (c2x,c2y) both equal (x,y), you get a linear
1701 interpolation ("lines")
1702 "velocity"/"V" d=[(x, y, vx, vy), ...]
1703 curve that passes through (x,y) with velocity (vx,vy)
1704 (one unit of arclength per unit time); in other words,
1705 (vx,vy) is the tangent vector at (x,y). If (vx,vy) is
1706 (0,0), you get a linear interpolation ("lines").
1707 "foreback"/"F" d=[(x, y, bx, by, fx, fy), ...]
1708 like "velocity" except that there is a left derivative
1709 (bx,by) and a right derivative (fx,fy). If (bx,by)
1710 equals (fx,fy) (with no minus sign), you get a
1711 "velocity" curve
1712 "smooth"/"S" d=[(x,y), (x,y), ...]
1713 a "velocity" interpolation with (vx,vy)[i] equal to
1714 ((x,y)[i+1] - (x,y)[i-1])/2: the minimal derivative
1716 defaults = {}
1718 def __repr__(self):
1719 return "<Poly (%d nodes) mode=%s loop=%s %s>" % (len(self.d), self.mode, repr(self.loop), self.attr)
1721 def __init__(self, d=[], mode="L", loop=False, **attr):
1722 self.d = list(d)
1723 self.mode = mode
1724 self.loop = loop
1726 self.attr = dict(self.defaults)
1727 self.attr.update(attr)
1729 def SVG(self, trans=None):
1730 """Apply the transformation "trans" and return an SVG object."""
1731 return self.Path(trans).SVG()
1733 def Path(self, trans=None, local=False):
1734 """Apply the transformation "trans" and return a Path object in
1735 global coordinates. If local=True, return a Path in local coordinates
1736 (which must be transformed again)."""
1737 if isinstance(trans, basestring): trans = totrans(trans)
1739 if self.mode[0] == "L" or self.mode[0] == "l": mode = "L"
1740 elif self.mode[0] == "B" or self.mode[0] == "b": mode = "B"
1741 elif self.mode[0] == "V" or self.mode[0] == "v": mode = "V"
1742 elif self.mode[0] == "F" or self.mode[0] == "f": mode = "F"
1743 elif self.mode[0] == "S" or self.mode[0] == "s":
1744 mode = "S"
1746 vx, vy = [0.]*len(self.d), [0.]*len(self.d)
1747 for i in xrange(len(self.d)):
1748 inext = (i+1) % len(self.d)
1749 iprev = (i-1) % len(self.d)
1751 vx[i] = (self.d[inext][0] - self.d[iprev][0])/2.
1752 vy[i] = (self.d[inext][1] - self.d[iprev][1])/2.
1753 if not self.loop and (i == 0 or i == len(self.d)-1):
1754 vx[i], vy[i] = 0., 0.
1756 else:
1757 raise ValueError, "mode must be \"lines\", \"bezier\", \"velocity\", \"foreback\", \"smooth\", or an abbreviation"
1759 d = []
1760 indexes = range(len(self.d))
1761 if self.loop and len(self.d) > 0: indexes.append(0)
1763 for i in indexes:
1764 inext = (i+1) % len(self.d)
1765 iprev = (i-1) % len(self.d)
1767 x, y = self.d[i][0], self.d[i][1]
1769 if trans == None: X, Y = x, y
1770 else: X, Y = trans(x, y)
1772 if d == []:
1773 if local: d.append(("M", x, y, False))
1774 else: d.append(("M", X, Y, True))
1776 elif mode == "L":
1777 if local: d.append(("L", x, y, False))
1778 else: d.append(("L", X, Y, True))
1780 elif mode == "B":
1781 c1x, c1y = self.d[i][2], self.d[i][3]
1782 if trans == None: C1X, C1Y = c1x, c1y
1783 else: C1X, C1Y = trans(c1x, c1y)
1785 c2x, c2y = self.d[i][4], self.d[i][5]
1786 if trans == None: C2X, C2Y = c2x, c2y
1787 else: C2X, C2Y = trans(c2x, c2y)
1789 if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1790 else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1792 elif mode == "V":
1793 c1x, c1y = self.d[iprev][2]/3. + self.d[iprev][0], self.d[iprev][3]/3. + self.d[iprev][1]
1794 c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
1796 if trans == None: C1X, C1Y = c1x, c1y
1797 else: C1X, C1Y = trans(c1x, c1y)
1798 if trans == None: C2X, C2Y = c2x, c2y
1799 else: C2X, C2Y = trans(c2x, c2y)
1801 if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1802 else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1804 elif mode == "F":
1805 c1x, c1y = self.d[iprev][4]/3. + self.d[iprev][0], self.d[iprev][5]/3. + self.d[iprev][1]
1806 c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
1808 if trans == None: C1X, C1Y = c1x, c1y
1809 else: C1X, C1Y = trans(c1x, c1y)
1810 if trans == None: C2X, C2Y = c2x, c2y
1811 else: C2X, C2Y = trans(c2x, c2y)
1813 if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1814 else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1816 elif mode == "S":
1817 c1x, c1y = vx[iprev]/3. + self.d[iprev][0], vy[iprev]/3. + self.d[iprev][1]
1818 c2x, c2y = vx[i]/-3. + x, vy[i]/-3. + y
1820 if trans == None: C1X, C1Y = c1x, c1y
1821 else: C1X, C1Y = trans(c1x, c1y)
1822 if trans == None: C2X, C2Y = c2x, c2y
1823 else: C2X, C2Y = trans(c2x, c2y)
1825 if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1826 else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1828 if self.loop and len(self.d) > 0: d.append(("Z",))
1830 return Path(d, **self.attr)
1832 ######################################################################
1834 class Text:
1835 """Draws at text string at a specified point in local coordinates.
1837 x, y required location of the point in local coordinates
1838 d required text/Unicode string
1839 attribute=value pairs keyword list SVG attributes
1842 defaults = {"stroke":"none", "fill":"black", "font-size":5}
1844 def __repr__(self):
1845 return "<Text %s at (%g, %g) %s>" % (repr(self.d), self.x, self.y, self.attr)
1847 def __init__(self, x, y, d, **attr):
1848 self.x = x
1849 self.y = y
1850 self.d = str(d)
1851 self.attr = dict(self.defaults)
1852 self.attr.update(attr)
1854 def SVG(self, trans=None):
1855 """Apply the transformation "trans" and return an SVG object."""
1856 if isinstance(trans, basestring): trans = totrans(trans)
1858 X, Y = self.x, self.y
1859 if trans != None: X, Y = trans(X, Y)
1860 return SVG("text", self.d, x=X, y=Y, **self.attr)
1862 class TextGlobal:
1863 """Draws at text string at a specified point in global coordinates.
1865 x, y required location of the point in global coordinates
1866 d required text/Unicode string
1867 attribute=value pairs keyword list SVG attributes
1869 defaults = {"stroke":"none", "fill":"black", "font-size":5}
1871 def __repr__(self):
1872 return "<TextGlobal %s at (%s, %s) %s>" % (repr(self.d), str(self.x), str(self.y), self.attr)
1874 def __init__(self, x, y, d, **attr):
1875 self.x = x
1876 self.y = y
1877 self.d = str(d)
1878 self.attr = dict(self.defaults)
1879 self.attr.update(attr)
1881 def SVG(self, trans=None):
1882 """Apply the transformation "trans" and return an SVG object."""
1883 return SVG("text", self.d, x=self.x, y=self.y, **self.attr)
1885 ######################################################################
1887 _symbol_templates = {"dot": SVG("symbol", SVG("circle", cx=0, cy=0, r=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), \
1888 "box": SVG("symbol", SVG("rect", x1=-1, y1=-1, x2=1, y2=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), \
1889 "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"), \
1890 "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"), \
1893 def make_symbol(id, shape="dot", **attr):
1894 """Creates a new instance of an SVG symbol to avoid cross-linking objects.
1896 id required a new identifier (string/Unicode)
1897 shape default="dot" the shape name from _symbol_templates
1898 attribute=value list keyword list modify the SVG attributes of the new symbol
1900 output = copy.deepcopy(_symbol_templates[shape])
1901 for i in output.sub: i.attr.update(attr_preprocess(attr))
1902 output["id"] = id
1903 return output
1905 _circular_dot = make_symbol("circular_dot")
1907 class Dots:
1908 """Dots draws SVG symbols at a set of points.
1910 d required list of (x,y) points
1911 symbol default=None SVG symbol or a new identifier to
1912 label an auto-generated symbol;
1913 if None, use pre-defined _circular_dot
1914 width, height default=1, 1 width and height of the symbols
1915 in SVG coordinates
1916 attribute=value pairs keyword list SVG attributes
1918 defaults = {}
1920 def __repr__(self):
1921 return "<Dots (%d nodes) %s>" % (len(self.d), self.attr)
1923 def __init__(self, d=[], symbol=None, width=1., height=1., **attr):
1924 self.d = list(d)
1925 self.width = width
1926 self.height = height
1928 self.attr = dict(self.defaults)
1929 self.attr.update(attr)
1931 if symbol == None:
1932 self.symbol = _circular_dot
1933 elif isinstance(symbol, SVG):
1934 self.symbol = symbol
1935 else:
1936 self.symbol = make_symbol(symbol)
1938 def SVG(self, trans=None):
1939 """Apply the transformation "trans" and return an SVG object."""
1940 if isinstance(trans, basestring): trans = totrans(trans)
1942 output = SVG("g", SVG("defs", self.symbol))
1943 id = "#%s" % self.symbol["id"]
1945 for p in self.d:
1946 x, y = p[0], p[1]
1948 if trans == None: X, Y = x, y
1949 else: X, Y = trans(x, y)
1951 item = SVG("use", x=X, y=Y, xlink__href=id)
1952 if self.width != None: item["width"] = self.width
1953 if self.height != None: item["height"] = self.height
1954 output.append(item)
1956 return output
1958 ######################################################################
1960 _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"), \
1961 "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"), \
1964 def make_marker(id, shape, **attr):
1965 """Creates a new instance of an SVG marker to avoid cross-linking objects.
1967 id required a new identifier (string/Unicode)
1968 shape required the shape name from _marker_templates
1969 attribute=value list keyword list modify the SVG attributes of the new marker
1971 output = copy.deepcopy(_marker_templates[shape])
1972 for i in output.sub: i.attr.update(attr_preprocess(attr))
1973 output["id"] = id
1974 return output
1976 class Line(Curve):
1977 """Draws a line between two points.
1979 Line(x1, y1, x2, y2, arrow_start, arrow_end, attribute=value)
1981 x1, y1 required the starting point
1982 x2, y2 required the ending point
1983 arrow_start default=None if an identifier string/Unicode,
1984 draw a new arrow object at the
1985 beginning of the line; if a marker,
1986 draw that marker instead
1987 arrow_end default=None same for the end of the line
1988 attribute=value pairs keyword list SVG attributes
1990 defaults = {}
1992 def __repr__(self):
1993 return "<Line (%g, %g) to (%g, %g) %s>" % (self.x1, self.y1, self.x2, self.y2, self.attr)
1995 def __init__(self, x1, y1, x2, y2, arrow_start=None, arrow_end=None, **attr):
1996 self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
1997 self.arrow_start, self.arrow_end = arrow_start, arrow_end
1999 self.attr = dict(self.defaults)
2000 self.attr.update(attr)
2002 def SVG(self, trans=None):
2003 """Apply the transformation "trans" and return an SVG object."""
2005 line = self.Path(trans).SVG()
2007 if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
2008 defs = SVG("defs")
2010 if self.arrow_start != False and self.arrow_start != None:
2011 if isinstance(self.arrow_start, SVG):
2012 defs.append(self.arrow_start)
2013 line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
2014 elif isinstance(self.arrow_start, basestring):
2015 defs.append(make_marker(self.arrow_start, "arrow_start"))
2016 line.attr["marker-start"] = "url(#%s)" % self.arrow_start
2017 else:
2018 raise TypeError, "arrow_start must be False/None or an id string for the new marker"
2020 if self.arrow_end != False and self.arrow_end != None:
2021 if isinstance(self.arrow_end, SVG):
2022 defs.append(self.arrow_end)
2023 line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
2024 elif isinstance(self.arrow_end, basestring):
2025 defs.append(make_marker(self.arrow_end, "arrow_end"))
2026 line.attr["marker-end"] = "url(#%s)" % self.arrow_end
2027 else:
2028 raise TypeError, "arrow_end must be False/None or an id string for the new marker"
2030 return SVG("g", defs, line)
2032 return line
2034 def Path(self, trans=None, local=False):
2035 """Apply the transformation "trans" and return a Path object in
2036 global coordinates. If local=True, return a Path in local coordinates
2037 (which must be transformed again)."""
2038 self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1 + t*(self.y2 - self.y1))
2039 self.low = 0.
2040 self.high = 1.
2041 self.loop = False
2043 if trans == None:
2044 return Path([("M", self.x1, self.y1, not local), ("L", self.x2, self.y2, not local)], **self.attr)
2045 else:
2046 return Curve.Path(self, trans, local)
2048 class LineGlobal:
2049 """Draws a line between two points, one or both of which is in
2050 global coordinates.
2052 Line(x1, y1, x2, y2, lcoal1, local2, arrow_start, arrow_end, attribute=value)
2054 x1, y1 required the starting point
2055 x2, y2 required the ending point
2056 local1 default=False if True, interpret first point as a
2057 local coordinate (apply transform)
2058 local2 default=False if True, interpret second point as a
2059 local coordinate (apply transform)
2060 arrow_start default=None if an identifier string/Unicode,
2061 draw a new arrow object at the
2062 beginning of the line; if a marker,
2063 draw that marker instead
2064 arrow_end default=None same for the end of the line
2065 attribute=value pairs keyword list SVG attributes
2067 defaults = {}
2069 def __repr__(self):
2070 local1, local2 = "", ""
2071 if self.local1: local1 = "L"
2072 if self.local2: local2 = "L"
2074 return "<LineGlobal %s(%s, %s) to %s(%s, %s) %s>" % (local1, str(self.x1), str(self.y1), local2, str(self.x2), str(self.y2), self.attr)
2076 def __init__(self, x1, y1, x2, y2, local1=False, local2=False, arrow_start=None, arrow_end=None, **attr):
2077 self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
2078 self.local1, self.local2 = local1, local2
2079 self.arrow_start, self.arrow_end = arrow_start, arrow_end
2081 self.attr = dict(self.defaults)
2082 self.attr.update(attr)
2084 def SVG(self, trans=None):
2085 """Apply the transformation "trans" and return an SVG object."""
2086 if isinstance(trans, basestring): trans = totrans(trans)
2088 X1, Y1, X2, Y2 = self.x1, self.y1, self.x2, self.y2
2090 if self.local1: X1, Y1 = trans(X1, Y1)
2091 if self.local2: X2, Y2 = trans(X2, Y2)
2093 line = SVG("path", d="M%s %s L%s %s" % (X1, Y1, X2, Y2), **self.attr)
2095 if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
2096 defs = SVG("defs")
2098 if self.arrow_start != False and self.arrow_start != None:
2099 if isinstance(self.arrow_start, SVG):
2100 defs.append(self.arrow_start)
2101 line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
2102 elif isinstance(self.arrow_start, basestring):
2103 defs.append(make_marker(self.arrow_start, "arrow_start"))
2104 line.attr["marker-start"] = "url(#%s)" % self.arrow_start
2105 else:
2106 raise TypeError, "arrow_start must be False/None or an id string for the new marker"
2108 if self.arrow_end != False and self.arrow_end != None:
2109 if isinstance(self.arrow_end, SVG):
2110 defs.append(self.arrow_end)
2111 line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
2112 elif isinstance(self.arrow_end, basestring):
2113 defs.append(make_marker(self.arrow_end, "arrow_end"))
2114 line.attr["marker-end"] = "url(#%s)" % self.arrow_end
2115 else:
2116 raise TypeError, "arrow_end must be False/None or an id string for the new marker"
2118 return SVG("g", defs, line)
2120 return line
2122 class VLine(Line):
2123 """Draws a vertical line.
2125 VLine(y1, y2, x, attribute=value)
2127 y1, y2 required y range
2128 x required x position
2129 attribute=value pairs keyword list SVG attributes
2131 defaults = {}
2133 def __repr__(self):
2134 return "<VLine (%g, %g) at x=%s %s>" % (self.y1, self.y2, self.x, self.attr)
2136 def __init__(self, y1, y2, x, **attr):
2137 self.x = x
2138 self.attr = dict(self.defaults)
2139 self.attr.update(attr)
2140 Line.__init__(self, x, y1, x, y2, **self.attr)
2142 def Path(self, trans=None, local=False):
2143 """Apply the transformation "trans" and return a Path object in
2144 global coordinates. If local=True, return a Path in local coordinates
2145 (which must be transformed again)."""
2146 self.x1 = self.x
2147 self.x2 = self.x
2148 return Line.Path(self, trans, local)
2150 class HLine(Line):
2151 """Draws a horizontal line.
2153 HLine(x1, x2, y, attribute=value)
2155 x1, x2 required x range
2156 y required y position
2157 attribute=value pairs keyword list SVG attributes
2159 defaults = {}
2161 def __repr__(self):
2162 return "<HLine (%g, %g) at y=%s %s>" % (self.x1, self.x2, self.y, self.attr)
2164 def __init__(self, x1, x2, y, **attr):
2165 self.y = y
2166 self.attr = dict(self.defaults)
2167 self.attr.update(attr)
2168 Line.__init__(self, x1, y, x2, y, **self.attr)
2170 def Path(self, trans=None, local=False):
2171 """Apply the transformation "trans" and return a Path object in
2172 global coordinates. If local=True, return a Path in local coordinates
2173 (which must be transformed again)."""
2174 self.y1 = self.y
2175 self.y2 = self.y
2176 return Line.Path(self, trans, local)
2178 ######################################################################
2180 class Rect(Curve):
2181 """Draws a rectangle.
2183 Rect(x1, y1, x2, y2, attribute=value)
2185 x1, y1 required the starting point
2186 x2, y2 required the ending point
2187 attribute=value pairs keyword list SVG attributes
2189 defaults = {}
2191 def __repr__(self):
2192 return "<Rect (%g, %g), (%g, %g) %s>" % (self.x1, self.y1, self.x2, self.y2, self.attr)
2194 def __init__(self, x1, y1, x2, y2, **attr):
2195 self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
2197 self.attr = dict(self.defaults)
2198 self.attr.update(attr)
2200 def SVG(self, trans=None):
2201 """Apply the transformation "trans" and return an SVG object."""
2202 return self.Path(trans).SVG()
2204 def Path(self, trans=None, local=False):
2205 """Apply the transformation "trans" and return a Path object in
2206 global coordinates. If local=True, return a Path in local coordinates
2207 (which must be transformed again)."""
2208 if trans == None:
2209 return Path([("M", self.x1, self.y1, not local), ("L", self.x2, self.y1, not local), ("L", self.x2, self.y2, not local), ("L", self.x1, self.y2, not local), ("Z",)], **self.attr)
2211 else:
2212 self.low = 0.
2213 self.high = 1.
2214 self.loop = False
2216 self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1)
2217 d1 = Curve.Path(self, trans, local).d
2219 self.f = lambda t: (self.x2, self.y1 + t*(self.y2 - self.y1))
2220 d2 = Curve.Path(self, trans, local).d
2221 del d2[0]
2223 self.f = lambda t: (self.x2 + t*(self.x1 - self.x2), self.y2)
2224 d3 = Curve.Path(self, trans, local).d
2225 del d3[0]
2227 self.f = lambda t: (self.x1, self.y2 + t*(self.y1 - self.y2))
2228 d4 = Curve.Path(self, trans, local).d
2229 del d4[0]
2231 return Path(d=(d1 + d2 + d3 + d4 + [("Z",)]), **self.attr)
2233 ######################################################################
2235 class Ellipse(Curve):
2236 """Draws an ellipse from a semimajor vector (ax,ay) and a semiminor
2237 length (b).
2239 Ellipse(x, y, ax, ay, b, attribute=value)
2241 x, y required the center of the ellipse/circle
2242 ax, ay required a vector indicating the length
2243 and direction of the semimajor axis
2244 b required the length of the semiminor axis.
2245 If equal to sqrt(ax2 + ay2), the
2246 ellipse is a circle
2247 attribute=value pairs keyword list SVG attributes
2249 (If sqrt(ax**2 + ay**2) is less than b, then (ax,ay) is actually the
2250 semiminor axis.)
2252 defaults = {}
2254 def __repr__(self):
2255 return "<Ellipse (%g, %g) a=(%g, %g), b=%g %s>" % (self.x, self.y, self.ax, self.ay, self.b, self.attr)
2257 def __init__(self, x, y, ax, ay, b, **attr):
2258 self.x, self.y, self.ax, self.ay, self.b = x, y, ax, ay, b
2260 self.attr = dict(self.defaults)
2261 self.attr.update(attr)
2263 def SVG(self, trans=None):
2264 """Apply the transformation "trans" and return an SVG object."""
2265 return self.Path(trans).SVG()
2267 def Path(self, trans=None, local=False):
2268 """Apply the transformation "trans" and return a Path object in
2269 global coordinates. If local=True, return a Path in local coordinates
2270 (which must be transformed again)."""
2271 angle = math.atan2(self.ay, self.ax) + math.pi/2.
2272 bx = self.b * math.cos(angle)
2273 by = self.b * math.sin(angle)
2275 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))
2276 self.low = -math.pi
2277 self.high = math.pi
2278 self.loop = True
2279 return Curve.Path(self, trans, local)
2281 ######################################################################
2283 def unumber(x):
2284 """Converts numbers to a Unicode string, taking advantage of special
2285 Unicode characters to make nice minus signs and scientific notation.
2287 output = u"%g" % x
2289 if output[0] == u"-":
2290 output = u"\u2013" + output[1:]
2292 index = output.find(u"e")
2293 if index != -1:
2294 uniout = unicode(output[:index]) + u"\u00d710"
2295 saw_nonzero = False
2296 for n in output[index+1:]:
2297 if n == u"+": pass # uniout += u"\u207a"
2298 elif n == u"-": uniout += u"\u207b"
2299 elif n == u"0":
2300 if saw_nonzero: uniout += u"\u2070"
2301 elif n == u"1":
2302 saw_nonzero = True
2303 uniout += u"\u00b9"
2304 elif n == u"2":
2305 saw_nonzero = True
2306 uniout += u"\u00b2"
2307 elif n == u"3":
2308 saw_nonzero = True
2309 uniout += u"\u00b3"
2310 elif u"4" <= n <= u"9":
2311 saw_nonzero = True
2312 if saw_nonzero: uniout += eval("u\"\\u%x\"" % (0x2070 + ord(n) - ord(u"0")))
2313 else: uniout += n
2315 if uniout[:2] == u"1\u00d7": uniout = uniout[2:]
2316 return uniout
2318 return output
2320 class Ticks:
2321 """Superclass for all graphics primatives that draw ticks,
2322 miniticks, and tick labels. This class only draws the ticks.
2324 Ticks(f, low, high, ticks, miniticks, labels, logbase, arrow_start,
2325 arrow_end, text_attr, attribute=value)
2327 f required parametric function along which ticks
2328 will be drawn; has the same format as
2329 the function used in Curve
2330 low, high required range of the independent variable
2331 ticks default=-10 request ticks according to the standard
2332 tick specification (see below)
2333 miniticks default=True request miniticks according to the
2334 standard minitick specification (below)
2335 labels True request tick labels according to the
2336 standard tick label specification (below)
2337 logbase default=None if a number, the axis is logarithmic with
2338 ticks at the given base (usually 10)
2339 arrow_start default=None if a new string identifier, draw an arrow
2340 at the low-end of the axis, referenced by
2341 that identifier; if an SVG marker object,
2342 use that marker
2343 arrow_end default=None if a new string identifier, draw an arrow
2344 at the high-end of the axis, referenced by
2345 that identifier; if an SVG marker object,
2346 use that marker
2347 text_attr default={} SVG attributes for the text labels
2348 attribute=value pairs keyword list SVG attributes for the tick marks
2350 Standard tick specification:
2352 * True: same as -10 (below).
2353 * Positive number N: draw exactly N ticks, including the endpoints. To
2354 subdivide an axis into 10 equal-sized segments, ask for 11 ticks.
2355 * Negative number -N: draw at least N ticks. Ticks will be chosen with
2356 "natural" values, multiples of 2 or 5.
2357 * List of values: draw a tick mark at each value.
2358 * Dict of value, label pairs: draw a tick mark at each value, labeling
2359 it with the given string. This lets you say things like {3.14159: "pi"}.
2360 * False or None: no ticks.
2362 Standard minitick specification:
2364 * True: draw miniticks with "natural" values, more closely spaced than
2365 the ticks.
2366 * Positive number N: draw exactly N miniticks, including the endpoints.
2367 To subdivide an axis into 100 equal-sized segments, ask for 101 miniticks.
2368 * Negative number -N: draw at least N miniticks.
2369 * List of values: draw a minitick mark at each value.
2370 * False or None: no miniticks.
2372 Standard tick label specification:
2374 * True: use the unumber function (described below)
2375 * Format string: standard format strings, e.g. "%5.2f" for 12.34
2376 * Python callable: function that converts numbers to strings
2377 * False or None: no labels
2379 defaults = {"stroke-width":"0.25pt"}
2380 text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
2381 tick_start = -1.5
2382 tick_end = 1.5
2383 minitick_start = -0.75
2384 minitick_end = 0.75
2385 text_start = 2.5
2386 text_angle = 0.
2388 def __repr__(self):
2389 return "<Ticks %s from %s to %s ticks=%s labels=%s %s>" % (self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr)
2391 def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, text_attr={}, **attr):
2392 self.f = f
2393 self.low = low
2394 self.high = high
2395 self.ticks = ticks
2396 self.miniticks = miniticks
2397 self.labels = labels
2398 self.logbase = logbase
2399 self.arrow_start = arrow_start
2400 self.arrow_end = arrow_end
2402 self.attr = dict(self.defaults)
2403 self.attr.update(attr)
2405 self.text_attr = dict(self.text_defaults)
2406 self.text_attr.update(text_attr)
2408 def orient_tickmark(self, t, trans=None):
2409 """Return the position, normalized local x vector, normalized
2410 local y vector, and angle of a tick at position t.
2412 Normally only used internally.
2414 if isinstance(trans, basestring): trans = totrans(trans)
2415 if trans == None:
2416 f = self.f
2417 else:
2418 f = lambda t: trans(*self.f(t))
2420 eps = _epsilon * abs(self.high - self.low)
2422 X, Y = f(t)
2423 Xprime, Yprime = f(t + eps)
2424 xhatx, xhaty = (Xprime - X)/eps, (Yprime - Y)/eps
2426 norm = math.sqrt(xhatx**2 + xhaty**2)
2427 xhatx, xhaty = xhatx/norm, xhaty/norm
2429 angle = math.atan2(xhaty, xhatx) + math.pi/2.
2430 yhatx, yhaty = math.cos(angle), math.sin(angle)
2432 return (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle
2434 def SVG(self, trans=None):
2435 """Apply the transformation "trans" and return an SVG object."""
2436 if isinstance(trans, basestring): trans = totrans(trans)
2438 self.last_ticks, self.last_miniticks = self.interpret()
2439 tickmarks = Path([], **self.attr)
2440 minitickmarks = Path([], **self.attr)
2441 output = SVG("g")
2443 if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
2444 defs = SVG("defs")
2446 if self.arrow_start != False and self.arrow_start != None:
2447 if isinstance(self.arrow_start, SVG):
2448 defs.append(self.arrow_start)
2449 elif isinstance(self.arrow_start, basestring):
2450 defs.append(make_marker(self.arrow_start, "arrow_start"))
2451 else:
2452 raise TypeError, "arrow_start must be False/None or an id string for the new marker"
2454 if self.arrow_end != False and self.arrow_end != None:
2455 if isinstance(self.arrow_end, SVG):
2456 defs.append(self.arrow_end)
2457 elif isinstance(self.arrow_end, basestring):
2458 defs.append(make_marker(self.arrow_end, "arrow_end"))
2459 else:
2460 raise TypeError, "arrow_end must be False/None or an id string for the new marker"
2462 output.append(defs)
2464 eps = _epsilon * (self.high - self.low)
2466 for t, label in self.last_ticks.items():
2467 (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
2469 if (not self.arrow_start or abs(t - self.low) > eps) and (not self.arrow_end or abs(t - self.high) > eps):
2470 tickmarks.d.append(("M", X - yhatx*self.tick_start, Y - yhaty*self.tick_start, True))
2471 tickmarks.d.append(("L", X - yhatx*self.tick_end, Y - yhaty*self.tick_end, True))
2473 angle = (angle - math.pi/2.)*180./math.pi + self.text_angle
2475 ########### a HACK! ############ (to be removed when Inkscape handles baselines)
2476 if _hacks["inkscape-text-vertical-shift"]:
2477 if self.text_start > 0:
2478 X += math.cos(angle*math.pi/180. + math.pi/2.) * 2.
2479 Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2.
2480 else:
2481 X += math.cos(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
2482 Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
2483 ########### end hack ###########
2485 if label != "":
2486 output.append(SVG("text", label, transform="translate(%g, %g) rotate(%g)" % \
2487 (X - yhatx*self.text_start, Y - yhaty*self.text_start, angle), **self.text_attr))
2489 for t in self.last_miniticks:
2490 skip = False
2491 for tt in self.last_ticks.keys():
2492 if abs(t - tt) < eps:
2493 skip = True
2494 break
2495 if not skip:
2496 (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
2498 if (not self.arrow_start or abs(t - self.low) > eps) and (not self.arrow_end or abs(t - self.high) > eps):
2499 minitickmarks.d.append(("M", X - yhatx*self.minitick_start, Y - yhaty*self.minitick_start, True))
2500 minitickmarks.d.append(("L", X - yhatx*self.minitick_end, Y - yhaty*self.minitick_end, True))
2502 output.prepend(tickmarks.SVG(trans))
2503 output.prepend(minitickmarks.SVG(trans))
2504 return output
2506 def interpret(self):
2507 """Evaluate and return optimal ticks and miniticks according to
2508 the standard minitick specification.
2510 Normally only used internally.
2513 if self.labels == None or self.labels == False:
2514 format = lambda x: ""
2516 elif self.labels == True:
2517 format = unumber
2519 elif isinstance(self.labels, basestring):
2520 format = lambda x: (self.labels % x)
2522 elif callable(self.labels):
2523 format = self.labels
2525 else: raise TypeError, "labels must be None/False, True, a format string, or a number->string function"
2527 # Now for the ticks
2528 ticks = self.ticks
2530 # Case 1: ticks is None/False
2531 if ticks == None or ticks == False: return {}, []
2533 # Case 2: ticks is the number of desired ticks
2534 elif isinstance(ticks, (int, long)):
2535 if ticks == True: ticks = -10
2537 if self.logbase == None:
2538 ticks = self.compute_ticks(ticks, format)
2539 else:
2540 ticks = self.compute_logticks(self.logbase, ticks, format)
2542 # Now for the miniticks
2543 if self.miniticks == True:
2544 if self.logbase == None:
2545 return ticks, self.compute_miniticks(ticks)
2546 else:
2547 return ticks, self.compute_logminiticks(self.logbase)
2549 elif isinstance(self.miniticks, (int, long)):
2550 return ticks, self.regular_miniticks(self.miniticks)
2552 elif getattr(self.miniticks, "__iter__", False):
2553 return ticks, self.miniticks
2555 elif self.miniticks == False or self.miniticks == None:
2556 return ticks, []
2558 else:
2559 raise TypeError, "miniticks must be None/False, True, a number of desired miniticks, or a list of numbers"
2561 # Cases 3 & 4: ticks is iterable
2562 elif getattr(ticks, "__iter__", False):
2564 # Case 3: ticks is some kind of list
2565 if not isinstance(ticks, dict):
2566 output = {}
2567 eps = _epsilon * (self.high - self.low)
2568 for x in ticks:
2569 if format == unumber and abs(x) < eps:
2570 output[x] = u"0"
2571 else:
2572 output[x] = format(x)
2573 ticks = output
2575 # Case 4: ticks is a dict
2576 else: pass
2578 # Now for the miniticks
2579 if self.miniticks == True:
2580 if self.logbase == None:
2581 return ticks, self.compute_miniticks(ticks)
2582 else:
2583 return ticks, self.compute_logminiticks(self.logbase)
2585 elif isinstance(self.miniticks, (int, long)):
2586 return ticks, self.regular_miniticks(self.miniticks)
2588 elif getattr(self.miniticks, "__iter__", False):
2589 return ticks, self.miniticks
2591 elif self.miniticks == False or self.miniticks == None:
2592 return ticks, []
2594 else:
2595 raise TypeError, "miniticks must be None/False, True, a number of desired miniticks, or a list of numbers"
2597 else:
2598 raise TypeError, "ticks must be None/False, a number of desired ticks, a list of numbers, or a dictionary of explicit markers"
2600 def compute_ticks(self, N, format):
2601 """Return less than -N or exactly N optimal linear ticks.
2603 Normally only used internally.
2605 if self.low >= self.high: raise ValueError, "low must be less than high"
2606 if N == 1: raise ValueError, "N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum"
2608 eps = _epsilon * (self.high - self.low)
2610 if N >= 0:
2611 output = {}
2612 x = self.low
2613 for i in xrange(N):
2614 if format == unumber and abs(x) < eps: label = u"0"
2615 else: label = format(x)
2616 output[x] = label
2617 x += (self.high - self.low)/(N-1.)
2618 return output
2620 N = -N
2622 counter = 0
2623 granularity = 10**math.ceil(math.log10(max(abs(self.low), abs(self.high))))
2624 lowN = math.ceil(1.*self.low / granularity)
2625 highN = math.floor(1.*self.high / granularity)
2627 while (lowN > highN):
2628 countermod3 = counter % 3
2629 if countermod3 == 0: granularity *= 0.5
2630 elif countermod3 == 1: granularity *= 0.4
2631 else: granularity *= 0.5
2632 counter += 1
2633 lowN = math.ceil(1.*self.low / granularity)
2634 highN = math.floor(1.*self.high / granularity)
2636 last_granularity = granularity
2637 last_trial = None
2639 while True:
2640 trial = {}
2641 for n in range(int(lowN), int(highN)+1):
2642 x = n * granularity
2643 if format == unumber and abs(x) < eps: label = u"0"
2644 else: label = format(x)
2645 trial[x] = label
2647 if int(highN)+1 - int(lowN) >= N:
2648 if last_trial == None:
2649 v1, v2 = self.low, self.high
2650 return {v1: format(v1), v2: format(v2)}
2651 else:
2652 low_in_ticks, high_in_ticks = False, False
2653 for t in last_trial.keys():
2654 if 1.*abs(t - self.low)/last_granularity < _epsilon: low_in_ticks = True
2655 if 1.*abs(t - self.high)/last_granularity < _epsilon: high_in_ticks = True
2657 lowN = 1.*self.low / last_granularity
2658 highN = 1.*self.high / last_granularity
2659 if abs(lowN - round(lowN)) < _epsilon and not low_in_ticks:
2660 last_trial[self.low] = format(self.low)
2661 if abs(highN - round(highN)) < _epsilon and not high_in_ticks:
2662 last_trial[self.high] = format(self.high)
2663 return last_trial
2665 last_granularity = granularity
2666 last_trial = trial
2668 countermod3 = counter % 3
2669 if countermod3 == 0: granularity *= 0.5
2670 elif countermod3 == 1: granularity *= 0.4
2671 else: granularity *= 0.5
2672 counter += 1
2673 lowN = math.ceil(1.*self.low / granularity)
2674 highN = math.floor(1.*self.high / granularity)
2676 def regular_miniticks(self, N):
2677 """Return exactly N linear ticks.
2679 Normally only used internally.
2681 output = []
2682 x = self.low
2683 for i in xrange(N):
2684 output.append(x)
2685 x += (self.high - self.low)/(N-1.)
2686 return output
2688 def compute_miniticks(self, original_ticks):
2689 """Return optimal linear miniticks, given a set of ticks.
2691 Normally only used internally.
2693 if len(original_ticks) < 2: original_ticks = ticks(self.low, self.high)
2694 original_ticks = original_ticks.keys()
2695 original_ticks.sort()
2697 if self.low > original_ticks[0] + _epsilon or self.high < original_ticks[-1] - _epsilon:
2698 raise ValueError, "original_ticks {%g...%g} extend beyond [%g, %g]" % (original_ticks[0], original_ticks[-1], self.low, self.high)
2700 granularities = []
2701 for i in range(len(original_ticks)-1):
2702 granularities.append(original_ticks[i+1] - original_ticks[i])
2703 spacing = 10**(math.ceil(math.log10(min(granularities)) - 1))
2705 output = []
2706 x = original_ticks[0] - math.ceil(1.*(original_ticks[0] - self.low) / spacing) * spacing
2708 while x <= self.high:
2709 if x >= self.low:
2710 already_in_ticks = False
2711 for t in original_ticks:
2712 if abs(x-t) < _epsilon * (self.high - self.low): already_in_ticks = True
2713 if not already_in_ticks: output.append(x)
2714 x += spacing
2715 return output
2717 def compute_logticks(self, base, N, format):
2718 """Return less than -N or exactly N optimal logarithmic ticks.
2720 Normally only used internally.
2722 if self.low >= self.high: raise ValueError, "low must be less than high"
2723 if N == 1: raise ValueError, "N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum"
2725 eps = _epsilon * (self.high - self.low)
2727 if N >= 0:
2728 output = {}
2729 x = self.low
2730 for i in xrange(N):
2731 if format == unumber and abs(x) < eps: label = u"0"
2732 else: label = format(x)
2733 output[x] = label
2734 x += (self.high - self.low)/(N-1.)
2735 return output
2737 N = -N
2739 lowN = math.floor(math.log(self.low, base))
2740 highN = math.ceil(math.log(self.high, base))
2741 output = {}
2742 for n in range(int(lowN), int(highN)+1):
2743 x = base**n
2744 label = format(x)
2745 if self.low <= x <= self.high: output[x] = label
2747 for i in range(1, len(output)):
2748 keys = output.keys()
2749 keys.sort()
2750 keys = keys[::i]
2751 values = map(lambda k: output[k], keys)
2752 if len(values) <= N:
2753 for k in output.keys():
2754 if k not in keys:
2755 output[k] = ""
2756 break
2758 if len(output) <= 2:
2759 output2 = compute_ticks(N=-int(math.ceil(N/2.)), format=format)
2760 lowest = min(output2)
2762 for k in output:
2763 if k < lowest: output2[k] = output[k]
2764 output = output2
2766 return output
2768 def compute_logminiticks(self, base):
2769 """Return optimal logarithmic miniticks, given a set of ticks.
2771 Normally only used internally.
2773 if self.low >= self.high: raise ValueError, "low must be less than high"
2775 lowN = math.floor(math.log(self.low, base))
2776 highN = math.ceil(math.log(self.high, base))
2777 output = []
2778 num_ticks = 0
2779 for n in range(int(lowN), int(highN)+1):
2780 x = base**n
2781 if self.low <= x <= self.high: num_ticks += 1
2782 for m in range(2, int(math.ceil(base))):
2783 minix = m * x
2784 if self.low <= minix <= self.high: output.append(minix)
2786 if num_ticks <= 2: return []
2787 else: return output
2789 ######################################################################
2791 class CurveAxis(Curve, Ticks):
2792 """Draw an axis with tick marks along a parametric curve.
2794 CurveAxis(f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
2795 text_attr, attribute=value)
2797 f required a Python callable or string in
2798 the form "f(t), g(t)", just like Curve
2799 low, high required left and right endpoints
2800 ticks default=-10 request ticks according to the standard
2801 tick specification (see help(Ticks))
2802 miniticks default=True request miniticks according to the
2803 standard minitick specification
2804 labels True request tick labels according to the
2805 standard tick label specification
2806 logbase default=None if a number, the x axis is logarithmic
2807 with ticks at the given base (10 being
2808 the most common)
2809 arrow_start default=None if a new string identifier, draw an
2810 arrow at the low-end of the axis,
2811 referenced by that identifier; if an
2812 SVG marker object, use that marker
2813 arrow_end default=None if a new string identifier, draw an
2814 arrow at the high-end of the axis,
2815 referenced by that identifier; if an
2816 SVG marker object, use that marker
2817 text_attr default={} SVG attributes for the text labels
2818 attribute=value pairs keyword list SVG attributes
2820 defaults = {"stroke-width":"0.25pt"}
2821 text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
2823 def __repr__(self):
2824 return "<CurveAxis %s [%s, %s] ticks=%s labels=%s %s>" % (self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr)
2826 def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, text_attr={}, **attr):
2827 tattr = dict(self.text_defaults)
2828 tattr.update(text_attr)
2829 Curve.__init__(self, f, low, high)
2830 Ticks.__init__(self, f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
2832 def SVG(self, trans=None):
2833 """Apply the transformation "trans" and return an SVG object."""
2834 func = Curve.SVG(self, trans)
2835 ticks = Ticks.SVG(self, trans) # returns a <g />
2837 if self.arrow_start != False and self.arrow_start != None:
2838 if isinstance(self.arrow_start, basestring):
2839 func.attr["marker-start"] = "url(#%s)" % self.arrow_start
2840 else:
2841 func.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
2843 if self.arrow_end != False and self.arrow_end != None:
2844 if isinstance(self.arrow_end, basestring):
2845 func.attr["marker-end"] = "url(#%s)" % self.arrow_end
2846 else:
2847 func.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
2849 ticks.append(func)
2850 return ticks
2852 class LineAxis(Line, Ticks):
2853 """Draws an axis with tick marks along a line.
2855 LineAxis(x1, y1, x2, y2, start, end, ticks, miniticks, labels, logbase,
2856 arrow_start, arrow_end, text_attr, attribute=value)
2858 x1, y1 required starting point
2859 x2, y2 required ending point
2860 start, end default=0, 1 values to start and end labeling
2861 ticks default=-10 request ticks according to the standard
2862 tick specification (see help(Ticks))
2863 miniticks default=True request miniticks according to the
2864 standard minitick specification
2865 labels True request tick labels according to the
2866 standard tick label specification
2867 logbase default=None if a number, the x axis is logarithmic
2868 with ticks at the given base (usually 10)
2869 arrow_start default=None if a new string identifier, draw an arrow
2870 at the low-end of the axis, referenced by
2871 that identifier; if an SVG marker object,
2872 use that marker
2873 arrow_end default=None if a new string identifier, draw an arrow
2874 at the high-end of the axis, referenced by
2875 that identifier; if an SVG marker object,
2876 use that marker
2877 text_attr default={} SVG attributes for the text labels
2878 attribute=value pairs keyword list SVG attributes
2880 defaults = {"stroke-width":"0.25pt"}
2881 text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
2883 def __repr__(self):
2884 return "<LineAxis (%g, %g) to (%g, %g) ticks=%s labels=%s %s>" % (self.x1, self.y1, self.x2, self.y2, str(self.ticks), str(self.labels), self.attr)
2886 def __init__(self, x1, y1, x2, y2, start=0., end=1., ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
2887 self.start = start
2888 self.end = end
2889 self.exclude = exclude
2890 tattr = dict(self.text_defaults)
2891 tattr.update(text_attr)
2892 Line.__init__(self, x1, y1, x2, y2, **attr)
2893 Ticks.__init__(self, None, None, None, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
2895 def interpret(self):
2896 if self.exclude != None and not (isinstance(self.exclude, (tuple, list)) and len(self.exclude) == 2 and \
2897 isinstance(self.exclude[0], (int, long, float)) and isinstance(self.exclude[1], (int, long, float))):
2898 raise TypeError, "exclude must either be None or (low, high)"
2900 ticks, miniticks = Ticks.interpret(self)
2901 if self.exclude == None: return ticks, miniticks
2903 ticks2 = {}
2904 for loc, label in ticks.items():
2905 if self.exclude[0] <= loc <= self.exclude[1]:
2906 ticks2[loc] = ""
2907 else:
2908 ticks2[loc] = label
2910 return ticks2, miniticks
2912 def SVG(self, trans=None):
2913 """Apply the transformation "trans" and return an SVG object."""
2914 line = Line.SVG(self, trans) # must be evaluated first, to set self.f, self.low, self.high
2916 f01 = self.f
2917 self.f = lambda t: f01(1. * (t - self.start) / (self.end - self.start))
2918 self.low = self.start
2919 self.high = self.end
2921 if self.arrow_start != False and self.arrow_start != None:
2922 if isinstance(self.arrow_start, basestring):
2923 line.attr["marker-start"] = "url(#%s)" % self.arrow_start
2924 else:
2925 line.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
2927 if self.arrow_end != False and self.arrow_end != None:
2928 if isinstance(self.arrow_end, basestring):
2929 line.attr["marker-end"] = "url(#%s)" % self.arrow_end
2930 else:
2931 line.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
2933 ticks = Ticks.SVG(self, trans) # returns a <g />
2934 ticks.append(line)
2935 return ticks
2937 class XAxis(LineAxis):
2938 """Draws an x axis with tick marks.
2940 XAxis(xmin, xmax, aty, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
2941 exclude, text_attr, attribute=value)
2943 xmin, xmax required the x range
2944 aty default=0 y position to draw the axis
2945 ticks default=-10 request ticks according to the standard
2946 tick specification (see help(Ticks))
2947 miniticks default=True request miniticks according to the
2948 standard minitick specification
2949 labels True request tick labels according to the
2950 standard tick label specification
2951 logbase default=None if a number, the x axis is logarithmic
2952 with ticks at the given base (usually 10)
2953 arrow_start default=None if a new string identifier, draw an arrow
2954 at the low-end of the axis, referenced by
2955 that identifier; if an SVG marker object,
2956 use that marker
2957 arrow_end default=None if a new string identifier, draw an arrow
2958 at the high-end of the axis, referenced by
2959 that identifier; if an SVG marker object,
2960 use that marker
2961 exclude default=None if a (low, high) pair, don't draw text
2962 labels within this range
2963 text_attr default={} SVG attributes for the text labels
2964 attribute=value pairs keyword list SVG attributes for all lines
2966 The exclude option is provided for Axes to keep text from overlapping
2967 where the axes cross. Normal users are not likely to need it.
2969 defaults = {"stroke-width":"0.25pt"}
2970 text_defaults = {"stroke":"none", "fill":"black", "font-size":5, "dominant-baseline":"text-before-edge"}
2971 text_start = -1.
2972 text_angle = 0.
2974 def __repr__(self):
2975 return "<XAxis (%g, %g) at y=%g ticks=%s labels=%s %s>" % (self.xmin, self.xmax, self.aty, str(self.ticks), str(self.labels), self.attr)
2977 def __init__(self, xmin, xmax, aty=0, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
2978 self.aty = aty
2979 tattr = dict(self.text_defaults)
2980 tattr.update(text_attr)
2981 LineAxis.__init__(self, xmin, aty, xmax, aty, xmin, xmax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
2983 def SVG(self, trans=None):
2984 """Apply the transformation "trans" and return an SVG object."""
2985 self.y1 = self.aty
2986 self.y2 = self.aty
2987 return LineAxis.SVG(self, trans)
2989 class YAxis(LineAxis):
2990 """Draws a y axis with tick marks.
2992 YAxis(ymin, ymax, atx, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
2993 exclude, text_attr, attribute=value)
2995 ymin, ymax required the y range
2996 atx default=0 x position to draw the axis
2997 ticks default=-10 request ticks according to the standard
2998 tick specification (see help(Ticks))
2999 miniticks default=True request miniticks according to the
3000 standard minitick specification
3001 labels True request tick labels according to the
3002 standard tick label specification
3003 logbase default=None if a number, the y axis is logarithmic
3004 with ticks at the given base (usually 10)
3005 arrow_start default=None if a new string identifier, draw an arrow
3006 at the low-end of the axis, referenced by
3007 that identifier; if an SVG marker object,
3008 use that marker
3009 arrow_end default=None if a new string identifier, draw an arrow
3010 at the high-end of the axis, referenced by
3011 that identifier; if an SVG marker object,
3012 use that marker
3013 exclude default=None if a (low, high) pair, don't draw text
3014 labels within this range
3015 text_attr default={} SVG attributes for the text labels
3016 attribute=value pairs keyword list SVG attributes for all lines
3018 The exclude option is provided for Axes to keep text from overlapping
3019 where the axes cross. Normal users are not likely to need it.
3021 defaults = {"stroke-width":"0.25pt"}
3022 text_defaults = {"stroke":"none", "fill":"black", "font-size":5, "text-anchor":"end", "dominant-baseline":"middle"}
3023 text_start = 2.5
3024 text_angle = 90.
3026 def __repr__(self):
3027 return "<YAxis (%g, %g) at x=%g ticks=%s labels=%s %s>" % (self.ymin, self.ymax, self.atx, str(self.ticks), str(self.labels), self.attr)
3029 def __init__(self, ymin, ymax, atx=0, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
3030 self.atx = atx
3031 tattr = dict(self.text_defaults)
3032 tattr.update(text_attr)
3033 LineAxis.__init__(self, atx, ymin, atx, ymax, ymin, ymax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
3035 def SVG(self, trans=None):
3036 """Apply the transformation "trans" and return an SVG object."""
3037 self.x1 = self.atx
3038 self.x2 = self.atx
3039 return LineAxis.SVG(self, trans)
3041 class Axes:
3042 """Draw a pair of intersecting x-y axes.
3044 Axes(xmin, xmax, ymin, ymax, atx, aty, xticks, xminiticks, xlabels, xlogbase,
3045 yticks, yminiticks, ylabels, ylogbase, arrows, text_attr, attribute=value)
3047 xmin, xmax required the x range
3048 ymin, ymax required the y range
3049 atx, aty default=0, 0 point where the axes try to cross;
3050 if outside the range, the axes will
3051 cross at the closest corner
3052 xticks default=-10 request ticks according to the standard
3053 tick specification (see help(Ticks))
3054 xminiticks default=True request miniticks according to the
3055 standard minitick specification
3056 xlabels True request tick labels according to the
3057 standard tick label specification
3058 xlogbase default=None if a number, the x axis is logarithmic
3059 with ticks at the given base (usually 10)
3060 yticks default=-10 request ticks according to the standard
3061 tick specification
3062 yminiticks default=True request miniticks according to the
3063 standard minitick specification
3064 ylabels True request tick labels according to the
3065 standard tick label specification
3066 ylogbase default=None if a number, the y axis is logarithmic
3067 with ticks at the given base (usually 10)
3068 arrows default=None if a new string identifier, draw arrows
3069 referenced by that identifier
3070 text_attr default={} SVG attributes for the text labels
3071 attribute=value pairs keyword list SVG attributes for all lines
3073 defaults = {"stroke-width":"0.25pt"}
3074 text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
3076 def __repr__(self):
3077 return "<Axes x=(%g, %g) y=(%g, %g) at (%g, %g) %s>" % (self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty, self.attr)
3079 def __init__(self, xmin, xmax, ymin, ymax, atx=0, aty=0, xticks=-10, xminiticks=True, xlabels=True, xlogbase=None, yticks=-10, yminiticks=True, ylabels=True, ylogbase=None, arrows=None, text_attr={}, **attr):
3080 self.xmin, self.xmax = xmin, xmax
3081 self.ymin, self.ymax = ymin, ymax
3082 self.atx, self.aty = atx, aty
3083 self.xticks, self.xminiticks, self.xlabels, self.xlogbase = xticks, xminiticks, xlabels, xlogbase
3084 self.yticks, self.yminiticks, self.ylabels, self.ylogbase = yticks, yminiticks, ylabels, ylogbase
3085 self.arrows = arrows
3087 self.text_attr = dict(self.text_defaults)
3088 self.text_attr.update(text_attr)
3090 self.attr = dict(self.defaults)
3091 self.attr.update(attr)
3093 def SVG(self, trans=None):
3094 """Apply the transformation "trans" and return an SVG object."""
3095 atx, aty = self.atx, self.aty
3096 if atx < self.xmin: atx = self.xmin
3097 if atx > self.xmax: atx = self.xmax
3098 if aty < self.ymin: aty = self.ymin
3099 if aty > self.ymax: aty = self.ymax
3101 xmargin = 0.1 * abs(self.ymin - self.ymax)
3102 xexclude = atx - xmargin, atx + xmargin
3104 ymargin = 0.1 * abs(self.xmin - self.xmax)
3105 yexclude = aty - ymargin, aty + ymargin
3107 if self.arrows != None and self.arrows != False:
3108 xarrow_start = self.arrows + ".xstart"
3109 xarrow_end = self.arrows + ".xend"
3110 yarrow_start = self.arrows + ".ystart"
3111 yarrow_end = self.arrows + ".yend"
3112 else:
3113 xarrow_start = xarrow_end = yarrow_start = yarrow_end = None
3115 xaxis = XAxis(self.xmin, self.xmax, aty, self.xticks, self.xminiticks, self.xlabels, self.xlogbase, xarrow_start, xarrow_end, exclude=xexclude, text_attr=self.text_attr, **self.attr).SVG(trans)
3116 yaxis = YAxis(self.ymin, self.ymax, atx, self.yticks, self.yminiticks, self.ylabels, self.ylogbase, yarrow_start, yarrow_end, exclude=yexclude, text_attr=self.text_attr, **self.attr).SVG(trans)
3117 return SVG("g", *(xaxis.sub + yaxis.sub))
3119 ######################################################################
3121 class HGrid(Ticks):
3122 """Draws the horizontal lines of a grid over a specified region
3123 using the standard tick specification (see help(Ticks)) to place the
3124 grid lines.
3126 HGrid(xmin, xmax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
3128 xmin, xmax required the x range
3129 low, high required the y range
3130 ticks default=-10 request ticks according to the standard
3131 tick specification (see help(Ticks))
3132 miniticks default=False request miniticks according to the
3133 standard minitick specification
3134 logbase default=None if a number, the axis is logarithmic
3135 with ticks at the given base (usually 10)
3136 mini_attr default={} SVG attributes for the minitick-lines
3137 (if miniticks != False)
3138 attribute=value pairs keyword list SVG attributes for the major tick lines
3140 defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
3141 mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
3143 def __repr__(self):
3144 return "<HGrid x=(%g, %g) %g <= y <= %g ticks=%s miniticks=%s %s>" % (self.xmin, self.xmax, self.low, self.high, str(self.ticks), str(self.miniticks), self.attr)
3146 def __init__(self, xmin, xmax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
3147 self.xmin, self.xmax = xmin, xmax
3149 self.mini_attr = dict(self.mini_defaults)
3150 self.mini_attr.update(mini_attr)
3152 Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
3154 self.attr = dict(self.defaults)
3155 self.attr.update(attr)
3157 def SVG(self, trans=None):
3158 """Apply the transformation "trans" and return an SVG object."""
3159 self.last_ticks, self.last_miniticks = Ticks.interpret(self)
3161 ticksd = []
3162 for t in self.last_ticks.keys():
3163 ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3165 miniticksd = []
3166 for t in self.last_miniticks:
3167 miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3169 return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
3171 class VGrid(Ticks):
3172 """Draws the vertical lines of a grid over a specified region
3173 using the standard tick specification (see help(Ticks)) to place the
3174 grid lines.
3176 HGrid(ymin, ymax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
3178 ymin, ymax required the y range
3179 low, high required the x range
3180 ticks default=-10 request ticks according to the standard
3181 tick specification (see help(Ticks))
3182 miniticks default=False request miniticks according to the
3183 standard minitick specification
3184 logbase default=None if a number, the axis is logarithmic
3185 with ticks at the given base (usually 10)
3186 mini_attr default={} SVG attributes for the minitick-lines
3187 (if miniticks != False)
3188 attribute=value pairs keyword list SVG attributes for the major tick lines
3190 defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
3191 mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
3193 def __repr__(self):
3194 return "<VGrid y=(%g, %g) %g <= x <= %g ticks=%s miniticks=%s %s>" % (self.ymin, self.ymax, self.low, self.high, str(self.ticks), str(self.miniticks), self.attr)
3196 def __init__(self, ymin, ymax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
3197 self.ymin, self.ymax = ymin, ymax
3199 self.mini_attr = dict(self.mini_defaults)
3200 self.mini_attr.update(mini_attr)
3202 Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
3204 self.attr = dict(self.defaults)
3205 self.attr.update(attr)
3207 def SVG(self, trans=None):
3208 """Apply the transformation "trans" and return an SVG object."""
3209 self.last_ticks, self.last_miniticks = Ticks.interpret(self)
3211 ticksd = []
3212 for t in self.last_ticks.keys():
3213 ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3215 miniticksd = []
3216 for t in self.last_miniticks:
3217 miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3219 return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
3221 class Grid(Ticks):
3222 """Draws a grid over a specified region using the standard tick
3223 specification (see help(Ticks)) to place the grid lines.
3225 Grid(xmin, xmax, ymin, ymax, ticks, miniticks, logbase, mini_attr, attribute=value)
3227 xmin, xmax required the x range
3228 ymin, ymax required the y range
3229 ticks default=-10 request ticks according to the standard
3230 tick specification (see help(Ticks))
3231 miniticks default=False request miniticks according to the
3232 standard minitick specification
3233 logbase default=None if a number, the axis is logarithmic
3234 with ticks at the given base (usually 10)
3235 mini_attr default={} SVG attributes for the minitick-lines
3236 (if miniticks != False)
3237 attribute=value pairs keyword list SVG attributes for the major tick lines
3239 defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
3240 mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
3242 def __repr__(self):
3243 return "<Grid x=(%g, %g) y=(%g, %g) ticks=%s miniticks=%s %s>" % (self.xmin, self.xmax, self.ymin, self.ymax, str(self.ticks), str(self.miniticks), self.attr)
3245 def __init__(self, xmin, xmax, ymin, ymax, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
3246 self.xmin, self.xmax = xmin, xmax
3247 self.ymin, self.ymax = ymin, ymax
3249 self.mini_attr = dict(self.mini_defaults)
3250 self.mini_attr.update(mini_attr)
3252 Ticks.__init__(self, None, None, None, ticks, miniticks, None, logbase)
3254 self.attr = dict(self.defaults)
3255 self.attr.update(attr)
3257 def SVG(self, trans=None):
3258 """Apply the transformation "trans" and return an SVG object."""
3259 self.low, self.high = self.xmin, self.xmax
3260 self.last_xticks, self.last_xminiticks = Ticks.interpret(self)
3261 self.low, self.high = self.ymin, self.ymax
3262 self.last_yticks, self.last_yminiticks = Ticks.interpret(self)
3264 ticksd = []
3265 for t in self.last_xticks.keys():
3266 ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3267 for t in self.last_yticks.keys():
3268 ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3270 miniticksd = []
3271 for t in self.last_xminiticks:
3272 miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3273 for t in self.last_yminiticks:
3274 miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3276 return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
3278 ######################################################################
3280 class XErrorBars:
3281 """Draws x error bars at a set of points. This is usually used
3282 before (under) a set of Dots at the same points.
3284 XErrorBars(d, attribute=value)
3286 d required list of (x,y,xerr...) points
3287 attribute=value pairs keyword list SVG attributes
3289 If points in d have
3291 * 3 elements, the third is the symmetric error bar
3292 * 4 elements, the third and fourth are the asymmetric lower and
3293 upper error bar. The third element should be negative,
3294 e.g. (5, 5, -1, 2) is a bar from 4 to 7.
3295 * more than 4, a tick mark is placed at each value. This lets
3296 you nest errors from different sources, correlated and
3297 uncorrelated, statistical and systematic, etc.
3299 defaults = {"stroke-width":"0.25pt"}
3301 def __repr__(self):
3302 return "<XErrorBars (%d nodes)>" % len(self.d)
3304 def __init__(self, d=[], **attr):
3305 self.d = list(d)
3307 self.attr = dict(self.defaults)
3308 self.attr.update(attr)
3310 def SVG(self, trans=None):
3311 """Apply the transformation "trans" and return an SVG object."""
3312 if isinstance(trans, basestring): trans = totrans(trans) # only once
3314 output = SVG("g")
3315 for p in self.d:
3316 x, y = p[0], p[1]
3318 if len(p) == 3: bars = [x - p[2], x + p[2]]
3319 else: bars = [x + pi for pi in p[2:]]
3321 start, end = min(bars), max(bars)
3322 output.append(LineAxis(start, y, end, y, start, end, bars, False, False, **self.attr).SVG(trans))
3324 return output
3326 class YErrorBars:
3327 """Draws y error bars at a set of points. This is usually used
3328 before (under) a set of Dots at the same points.
3330 YErrorBars(d, attribute=value)
3332 d required list of (x,y,yerr...) points
3333 attribute=value pairs keyword list SVG attributes
3335 If points in d have
3337 * 3 elements, the third is the symmetric error bar
3338 * 4 elements, the third and fourth are the asymmetric lower and
3339 upper error bar. The third element should be negative,
3340 e.g. (5, 5, -1, 2) is a bar from 4 to 7.
3341 * more than 4, a tick mark is placed at each value. This lets
3342 you nest errors from different sources, correlated and
3343 uncorrelated, statistical and systematic, etc.
3345 defaults = {"stroke-width":"0.25pt"}
3347 def __repr__(self):
3348 return "<YErrorBars (%d nodes)>" % len(self.d)
3350 def __init__(self, d=[], **attr):
3351 self.d = list(d)
3353 self.attr = dict(self.defaults)
3354 self.attr.update(attr)
3356 def SVG(self, trans=None):
3357 """Apply the transformation "trans" and return an SVG object."""
3358 if isinstance(trans, basestring): trans = totrans(trans) # only once
3360 output = SVG("g")
3361 for p in self.d:
3362 x, y = p[0], p[1]
3364 if len(p) == 3: bars = [y - p[2], y + p[2]]
3365 else: bars = [y + pi for pi in p[2:]]
3367 start, end = min(bars), max(bars)
3368 output.append(LineAxis(x, start, x, end, start, end, bars, False, False, **self.attr).SVG(trans))
3370 return output