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