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