File indexing completed on 2024-09-26 05:05:38
0001
0002
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012
0013
0014
0015
0016
0017
0018
0019 from builtins import range
0020 import re, codecs, os, platform, copy, itertools, math, cmath, random, sys, copy
0021 _epsilon = 1e-5
0022
0023
0024 if re.search("windows", platform.system(), re.I):
0025 try:
0026 import _winreg
0027 _default_directory = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER, \
0028 r"Software\Microsoft\Windows\Current Version\Explorer\Shell Folders"), "Desktop")[0]
0029
0030
0031
0032 except:
0033 _default_directory = os.path.expanduser("~") + os.sep + "Desktop"
0034
0035 _default_fileName = "tmp.svg"
0036
0037 _hacks = {}
0038 _hacks["inkscape-text-vertical-shift"] = False
0039
0040 def rgb(r, g, b, maximum=1.):
0041 """Create an SVG color string "#xxyyzz" from r, g, and b.
0042
0043 r,g,b = 0 is black and r,g,b = maximum is white.
0044 """
0045 return "#%02x%02x%02x" % (max(0, min(r*255./maximum, 255)), max(0, min(g*255./maximum, 255)), max(0, min(b*255./maximum, 255)))
0046
0047 def attr_preprocess(attr):
0048 for name in attr.keys():
0049 name_colon = re.sub("__", ":", name)
0050 if name_colon != name:
0051 attr[name_colon] = attr[name]
0052 del attr[name]
0053 name = name_colon
0054
0055 name_dash = re.sub("_", "-", name)
0056 if name_dash != name:
0057 attr[name_dash] = attr[name]
0058 del attr[name]
0059 name = name_dash
0060
0061 return attr
0062
0063 class SVG:
0064 """A tree representation of an SVG image or image fragment.
0065
0066 SVG(t, sub, sub, sub..., attribute=value)
0067
0068 t required SVG type name
0069 sub optional list nested SVG elements or text/Unicode
0070 attribute=value pairs optional keywords SVG attributes
0071
0072 In attribute names, "__" becomes ":" and "_" becomes "-".
0073
0074 SVG in XML
0075
0076 <g id="mygroup" fill="blue">
0077 <rect x="1" y="1" width="2" height="2" />
0078 <rect x="3" y="3" width="2" height="2" />
0079 </g>
0080
0081 SVG in Python
0082
0083 >>> svg = SVG("g", SVG("rect", x=1, y=1, width=2, height=2), \\
0084 ... SVG("rect", x=3, y=3, width=2, height=2), \\
0085 ... id="mygroup", fill="blue")
0086
0087 Sub-elements and attributes may be accessed through tree-indexing:
0088
0089 >>> svg = SVG("text", SVG("tspan", "hello there"), stroke="none", fill="black")
0090 >>> svg[0]
0091 <tspan (1 sub) />
0092 >>> svg[0, 0]
0093 'hello there'
0094 >>> svg["fill"]
0095 'black'
0096
0097 Iteration is depth-first:
0098
0099 >>> svg = SVG("g", SVG("g", SVG("line", x1=0, y1=0, x2=1, y2=1)), \\
0100 ... SVG("text", SVG("tspan", "hello again")))
0101 ...
0102 >>> for ti, s in svg:
0103 ... print ti, repr(s)
0104 ...
0105 (0,) <g (1 sub) />
0106 (0, 0) <line x2=1 y1=0 x1=0 y2=1 />
0107 (0, 0, 'x2') 1
0108 (0, 0, 'y1') 0
0109 (0, 0, 'x1') 0
0110 (0, 0, 'y2') 1
0111 (1,) <text (1 sub) />
0112 (1, 0) <tspan (1 sub) />
0113 (1, 0, 0) 'hello again'
0114
0115 Use "print" to navigate:
0116
0117 >>> print svg
0118 None <g (2 sub) />
0119 [0] <g (1 sub) />
0120 [0, 0] <line x2=1 y1=0 x1=0 y2=1 />
0121 [1] <text (1 sub) />
0122 [1, 0] <tspan (1 sub) />
0123 """
0124 def __init__(self, *t_sub, **attr):
0125 if len(t_sub) == 0: raise TypeError("SVG element must have a t (SVG type)")
0126
0127
0128 self.t = t_sub[0]
0129
0130 self.sub = list(t_sub[1:])
0131
0132
0133
0134 self.attr = attr_preprocess(attr)
0135
0136 def __getitem__(self, ti):
0137 """Index is a list that descends tree, returning a sub-element if
0138 it ends with a number and an attribute if it ends with a string."""
0139 obj = self
0140 if isinstance(ti, (list, tuple)):
0141 for i in ti[:-1]: obj = obj[i]
0142 ti = ti[-1]
0143
0144 if isinstance(ti, (int, long, slice)): return obj.sub[ti]
0145 else: return obj.attr[ti]
0146
0147 def __setitem__(self, ti, value):
0148 """Index is a list that descends tree, returning a sub-element if
0149 it ends with a number and an attribute if it ends with a string."""
0150 obj = self
0151 if isinstance(ti, (list, tuple)):
0152 for i in ti[:-1]: obj = obj[i]
0153 ti = ti[-1]
0154
0155 if isinstance(ti, (int, long, slice)): obj.sub[ti] = value
0156 else: obj.attr[ti] = value
0157
0158 def __delitem__(self, ti):
0159 """Index is a list that descends tree, returning a sub-element if
0160 it ends with a number and an attribute if it ends with a string."""
0161 obj = self
0162 if isinstance(ti, (list, tuple)):
0163 for i in ti[:-1]: obj = obj[i]
0164 ti = ti[-1]
0165
0166 if isinstance(ti, (int, long, slice)): del obj.sub[ti]
0167 else: del obj.attr[ti]
0168
0169 def __contains__(self, value):
0170 """x in svg == True iff x is an attribute in svg."""
0171 return value in self.attr
0172
0173 def __eq__(self, other):
0174 """x == y iff x represents the same SVG as y."""
0175 if id(self) == id(other): return True
0176 return isinstance(other, SVG) and self.t == other.t and self.sub == other.sub and self.attr == other.attr
0177
0178 def __ne__(self, other):
0179 """x != y iff x does not represent the same SVG as y."""
0180 return not (self == other)
0181
0182 def append(self, x):
0183 """Appends x to the list of sub-elements (drawn last, overlaps
0184 other primatives)."""
0185 self.sub.append(x)
0186
0187 def prepend(self, x):
0188 """Prepends x to the list of sub-elements (drawn first may be
0189 overlapped by other primatives)."""
0190 self.sub[0:0] = [x]
0191
0192 def extend(self, x):
0193 """Extends list of sub-elements by a list x."""
0194 self.sub.extend(x)
0195
0196 def clone(self, shallow=False):
0197 """Deep copy of SVG tree. Set shallow=True for a shallow copy."""
0198 if shallow:
0199 return copy.copy(self)
0200 else:
0201 return copy.deepcopy(self)
0202
0203
0204 class SVGDepthIterator:
0205 """Manages SVG iteration."""
0206
0207 def __init__(self, svg, ti, depth_limit):
0208 self.svg = svg
0209 self.ti = ti
0210 self.shown = False
0211 self.depth_limit = depth_limit
0212
0213 def __iter__(self): return self
0214
0215 def next(self):
0216 if not self.shown:
0217 self.shown = True
0218 if self.ti != ():
0219 return self.ti, self.svg
0220
0221 if not isinstance(self.svg, SVG): raise StopIteration
0222 if self.depth_limit != None and len(self.ti) >= self.depth_limit: raise StopIteration
0223
0224 if "iterators" not in self.__dict__:
0225 self.iterators = []
0226 for i, s in enumerate(self.svg.sub):
0227 self.iterators.append(self.__class__(s, self.ti + (i,), self.depth_limit))
0228 for k, s in self.svg.attr.items():
0229 self.iterators.append(self.__class__(s, self.ti + (k,), self.depth_limit))
0230 self.iterators = itertools.chain(*self.iterators)
0231
0232 return next(self.iterators)
0233
0234
0235 def depth_first(self, depth_limit=None):
0236 """Returns a depth-first generator over the SVG. If depth_limit
0237 is a number, stop recursion at that depth."""
0238 return self.SVGDepthIterator(self, (), depth_limit)
0239
0240 def breadth_first(self, depth_limit=None):
0241 """Not implemented yet. Any ideas on how to do it?
0242
0243 Returns a breadth-first generator over the SVG. If depth_limit
0244 is a number, stop recursion at that depth."""
0245 raise NotImplementedError("Got an algorithm for breadth-first searching a tree without effectively copying the tree?")
0246
0247 def __iter__(self): return self.depth_first()
0248
0249 def items(self, sub=True, attr=True, text=True):
0250 """Get a recursively-generated list of tree-index, sub-element/attribute pairs.
0251
0252 If sub == False, do not show sub-elements.
0253 If attr == False, do not show attributes.
0254 If text == False, do not show text/Unicode sub-elements.
0255 """
0256 output = []
0257 for ti, s in self:
0258 show = False
0259 if isinstance(ti[-1], (int, long)):
0260 if isinstance(s, str): show = text
0261 else: show = sub
0262 else: show = attr
0263
0264 if show: output.append((ti, s))
0265 return output
0266
0267 def keys(self, sub=True, attr=True, text=True):
0268 """Get a recursively-generated list of tree-indexes.
0269
0270 If sub == False, do not show sub-elements.
0271 If attr == False, do not show attributes.
0272 If text == False, do not show text/Unicode sub-elements.
0273 """
0274 return [ti for ti, s in self.items(sub, attr, text)]
0275
0276 def values(self, sub=True, attr=True, text=True):
0277 """Get a recursively-generated list of sub-elements and attributes.
0278
0279 If sub == False, do not show sub-elements.
0280 If attr == False, do not show attributes.
0281 If text == False, do not show text/Unicode sub-elements.
0282 """
0283 return [s for ti, s in self.items(sub, attr, text)]
0284
0285 def __repr__(self): return self.xml(depth_limit=0)
0286
0287 def __str__(self):
0288 """Print (actually, return a string of) the tree in a form useful for browsing."""
0289 return self.tree(sub=True, attr=False, text=False)
0290
0291 def tree(self, depth_limit=None, sub=True, attr=True, text=True, tree_width=20, obj_width=80):
0292 """Print (actually, return a string of) the tree in a form useful for browsing.
0293
0294 If depth_limit == a number, stop recursion at that depth.
0295 If sub == False, do not show sub-elements.
0296 If attr == False, do not show attributes.
0297 If text == False, do not show text/Unicode sub-elements.
0298 tree_width is the number of characters reserved for printing tree indexes.
0299 obj_width is the number of characters reserved for printing sub-elements/attributes.
0300 """
0301
0302 output = []
0303
0304 line = "%s %s" % (("%%-%ds" % tree_width) % repr(None), ("%%-%ds" % obj_width) % (repr(self))[0:obj_width])
0305 output.append(line)
0306
0307 for ti, s in self.depth_first(depth_limit):
0308 show = False
0309 if isinstance(ti[-1], (int, long)):
0310 if isinstance(s, str): show = text
0311 else: show = sub
0312 else: show = attr
0313
0314 if show:
0315 line = "%s %s" % (("%%-%ds" % tree_width) % repr(list(ti)), ("%%-%ds" % obj_width) % (" "*len(ti) + repr(s))[0:obj_width])
0316 output.append(line)
0317
0318 return "\n".join(output)
0319
0320 def xml(self, indent=" ", newl="\n", depth_limit=None, depth=0):
0321 """Get an XML representation of the SVG.
0322
0323 indent string used for indenting
0324 newl string used for newlines
0325 If depth_limit == a number, stop recursion at that depth.
0326 depth starting depth (not useful for users)
0327
0328 print svg.xml()
0329 """
0330
0331 attrstr = []
0332 for n, v in self.attr.items():
0333 if isinstance(v, dict):
0334 v = "; ".join(["%s:%s" % (ni, vi) for ni, vi in v.items()])
0335 elif isinstance(v, (list, tuple)):
0336 v = ", ".join(v)
0337 attrstr.append(" %s=%s" % (n, repr(v)))
0338 attrstr = "".join(attrstr)
0339
0340 if len(self.sub) == 0: return "%s<%s%s />" % (indent * depth, self.t, attrstr)
0341
0342 if depth_limit == None or depth_limit > depth:
0343 substr = []
0344 for s in self.sub:
0345 if isinstance(s, SVG):
0346 substr.append(s.xml(indent, newl, depth_limit, depth + 1) + newl)
0347 elif isinstance(s, str):
0348 substr.append("%s%s%s" % (indent * (depth + 1), s, newl))
0349 else:
0350 substr.append("%s%s%s" % (indent * (depth + 1), repr(s), newl))
0351 substr = "".join(substr)
0352
0353 return "%s<%s%s>%s%s%s</%s>" % (indent * depth, self.t, attrstr, newl, substr, indent * depth, self.t)
0354
0355 else:
0356 return "%s<%s (%d sub)%s />" % (indent * depth, self.t, len(self.sub), attrstr)
0357
0358 def standalone_xml(self, indent=" ", newl="\n"):
0359 """Get an XML representation of the SVG that can be saved/rendered.
0360
0361 indent string used for indenting
0362 newl string used for newlines
0363 """
0364
0365 if self.t == "svg": top = self
0366 else: top = canvas(self)
0367 return """\
0368 <?xml version="1.0" standalone="no"?>
0369 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
0370
0371 """ + ("".join(top.__standalone_xml(indent, newl)))
0372
0373 def __standalone_xml(self, indent, newl):
0374 output = [u"<%s" % self.t]
0375
0376 for n, v in self.attr.items():
0377 if isinstance(v, dict):
0378 v = "; ".join(["%s:%s" % (ni, vi) for ni, vi in v.items()])
0379 elif isinstance(v, (list, tuple)):
0380 v = ", ".join(v)
0381 output.append(u" %s=\"%s\"" % (n, v))
0382
0383 if len(self.sub) == 0:
0384 output.append(u" />%s%s" % (newl, newl))
0385 return output
0386
0387 elif self.t == "text" or self.t == "tspan" or self.t == "style":
0388 output.append(u">")
0389
0390 else:
0391 output.append(u">%s%s" % (newl, newl))
0392
0393 for s in self.sub:
0394 if isinstance(s, SVG): output.extend(s.__standalone_xml(indent, newl))
0395 else: output.append(unicode(s))
0396
0397 if self.t == "tspan": output.append(u"</%s>" % self.t)
0398 else: output.append(u"</%s>%s%s" % (self.t, newl, newl))
0399
0400 return output
0401
0402 def interpret_fileName(self, fileName=None):
0403 if fileName == None:
0404 fileName = _default_fileName
0405 if re.search("windows", platform.system(), re.I) and not os.path.isabs(fileName):
0406 fileName = _default_directory + os.sep + fileName
0407 return fileName
0408
0409 def save(self, fileName=None, encoding="utf-8", compresslevel=None):
0410 """Save to a file for viewing. Note that svg.save() overwrites the file named _default_fileName.
0411
0412 fileName default=None note that _default_fileName will be overwritten if
0413 no fileName is specified. If the extension
0414 is ".svgz" or ".gz", the output will be gzipped
0415 encoding default="utf-8" file encoding (default is Unicode)
0416 compresslevel default=None if a number, the output will be gzipped with that
0417 compression level (1-9, 1 being fastest and 9 most
0418 thorough)
0419 """
0420 fileName = self.interpret_fileName(fileName)
0421
0422 if compresslevel != None or re.search("\\.svgz$", fileName, re.I) or re.search("\\.gz$", fileName, re.I):
0423 import gzip
0424 if compresslevel == None:
0425 f = gzip.GzipFile(fileName, "w")
0426 else:
0427 f = gzip.GzipFile(fileName, "w", compresslevel)
0428
0429 f = codecs.EncodedFile(f, "utf-8", encoding)
0430 f.write(self.standalone_xml())
0431 f.close()
0432
0433 else:
0434 f = codecs.open(fileName, "w", encoding=encoding)
0435 f.write(self.standalone_xml())
0436 f.close()
0437
0438 def inkview(self, fileName=None, encoding="utf-8"):
0439 """View in "inkview", assuming that program is available on your system.
0440
0441 fileName default=None note that any file named _default_fileName will be
0442 overwritten if no fileName is specified. If the extension
0443 is ".svgz" or ".gz", the output will be gzipped
0444 encoding default="utf-8" file encoding (default is Unicode)
0445 """
0446 fileName = self.interpret_fileName(fileName)
0447 self.save(fileName, encoding)
0448 os.spawnvp(os.P_NOWAIT, "inkview", ("inkview", fileName))
0449
0450 def inkscape(self, fileName=None, encoding="utf-8"):
0451 """View in "inkscape", assuming that program is available on your system.
0452
0453 fileName default=None note that any file named _default_fileName will be
0454 overwritten if no fileName is specified. If the extension
0455 is ".svgz" or ".gz", the output will be gzipped
0456 encoding default="utf-8" file encoding (default is Unicode)
0457 """
0458 fileName = self.interpret_fileName(fileName)
0459 self.save(fileName, encoding)
0460 os.spawnvp(os.P_NOWAIT, "inkscape", ("inkscape", fileName))
0461
0462 def firefox(self, fileName=None, encoding="utf-8"):
0463 """View in "firefox", assuming that program is available on your system.
0464
0465 fileName default=None note that any file named _default_fileName will be
0466 overwritten if no fileName is specified. If the extension
0467 is ".svgz" or ".gz", the output will be gzipped
0468 encoding default="utf-8" file encoding (default is Unicode)
0469 """
0470 fileName = self.interpret_fileName(fileName)
0471 self.save(fileName, encoding)
0472 os.spawnvp(os.P_NOWAIT, "firefox", ("firefox", fileName))
0473
0474
0475
0476 _canvas_defaults = {"width": "400px", "height": "400px", "viewBox": "0 0 100 100", \
0477 "xmlns": "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink", "version":"1.1", \
0478 "style": {"stroke":"black", "fill":"none", "stroke-width":"0.5pt", "stroke-linejoin":"round", "text-anchor":"middle"}, \
0479 "font-family": ["Helvetica", "Arial", "FreeSans", "Sans", "sans", "sans-serif"], \
0480 }
0481
0482 def canvas(*sub, **attr):
0483 """Creates a top-level SVG object, allowing the user to control the
0484 image size and aspect ratio.
0485
0486 canvas(sub, sub, sub..., attribute=value)
0487
0488 sub optional list nested SVG elements or text/Unicode
0489 attribute=value pairs optional keywords SVG attributes
0490
0491 Default attribute values:
0492
0493 width "400px"
0494 height "400px"
0495 viewBox "0 0 100 100"
0496 xmlns "http://www.w3.org/2000/svg"
0497 xmlns:xlink "http://www.w3.org/1999/xlink"
0498 version "1.1"
0499 style "stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoin:round; text-anchor:middle"
0500 font-family "Helvetica,Arial,FreeSans?,Sans,sans,sans-serif"
0501 """
0502 attributes = dict(_canvas_defaults)
0503 attributes.update(attr)
0504
0505 if sub == None or sub == ():
0506 return SVG("svg", **attributes)
0507 else:
0508 return SVG("svg", *sub, **attributes)
0509
0510 def canvas_outline(*sub, **attr):
0511 """Same as canvas(), but draws an outline around the drawable area,
0512 so that you know how close your image is to the edges."""
0513 svg = canvas(*sub, **attr)
0514 match = re.match("[, \t]*([0-9e.+\\-]+)[, \t]+([0-9e.+\\-]+)[, \t]+([0-9e.+\\-]+)[, \t]+([0-9e.+\\-]+)[, \t]*", svg["viewBox"])
0515 if match == None: raise ValueError("canvas viewBox is incorrectly formatted")
0516 x, y, width, height = [float(x) for x in match.groups()]
0517 svg.prepend(SVG("rect", x=x, y=y, width=width, height=height, stroke="none", fill="cornsilk"))
0518 svg.append(SVG("rect", x=x, y=y, width=width, height=height, stroke="black", fill="none"))
0519 return svg
0520
0521 def template(fileName, svg, replaceme="REPLACEME"):
0522 """Loads an SVG image from a file, replacing instances of
0523 <REPLACEME /> with a given svg object.
0524
0525 fileName required name of the template SVG
0526 svg required SVG object for replacement
0527 replaceme default="REPLACEME" fake SVG element to be replaced by the given object
0528
0529 >>> print load("template.svg")
0530 None <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
0531 [0] <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
0532 [1] <REPLACEME />
0533 >>>
0534 >>> print template("template.svg", SVG("circle", cx=50, cy=50, r=30))
0535 None <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
0536 [0] <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
0537 [1] <circle cy=50 cx=50 r=30 />
0538 """
0539 output = load(fileName)
0540 for ti, s in output:
0541 if isinstance(s, SVG) and s.t == replaceme:
0542 output[ti] = svg
0543 return output
0544
0545
0546
0547 def load(fileName):
0548 """Loads an SVG image from a file."""
0549 return load_stream(file(fileName))
0550
0551 def load_stream(stream):
0552 """Loads an SVG image from a stream (can be a string or a file object)."""
0553
0554 from xml.sax import handler, make_parser
0555 from xml.sax.handler import feature_namespaces, feature_external_ges, feature_external_pes
0556
0557 class ContentHandler(handler.ContentHandler):
0558 def __init__(self):
0559 self.stack = []
0560 self.output = None
0561 self.all_whitespace = re.compile("^\\s*$")
0562
0563 def startElement(self, name, attr):
0564 s = SVG(name)
0565 s.attr = dict(attr.items())
0566 if len(self.stack) > 0:
0567 last = self.stack[-1]
0568 last.sub.append(s)
0569 self.stack.append(s)
0570
0571 def characters(self, ch):
0572 if not isinstance(ch, str) or self.all_whitespace.match(ch) == None:
0573 if len(self.stack) > 0:
0574 last = self.stack[-1]
0575 if len(last.sub) > 0 and isinstance(last.sub[-1], str):
0576 last.sub[-1] = last.sub[-1] + "\n" + ch
0577 else:
0578 last.sub.append(ch)
0579
0580 def endElement(self, name):
0581 if len(self.stack) > 0:
0582 last = self.stack[-1]
0583 if isinstance(last, SVG) and last.t == "style" and "type" in last.attr and last.attr["type"] == "text/css" and len(last.sub) == 1 and isinstance(last.sub[0], str):
0584 last.sub[0] = "<![CDATA[\n" + last.sub[0] + "]]>"
0585
0586 self.output = self.stack.pop()
0587
0588 ch = ContentHandler()
0589 parser = make_parser()
0590 parser.setContentHandler(ch)
0591 parser.setFeature(feature_namespaces, 0)
0592 parser.setFeature(feature_external_ges, 0)
0593 parser.parse(stream)
0594 return ch.output
0595
0596
0597
0598 def totrans(expr, vars=("x", "y"), globals=None, locals=None):
0599 """Converts to a coordinate transformation (a function that accepts
0600 two arguments and returns two values).
0601
0602 expr required a string expression or a function
0603 of two real or one complex value
0604 vars default=("x", "y") independent variable names;
0605 a singleton ("z",) is interpreted
0606 as complex
0607 globals default=None dict of global variables
0608 locals default=None dict of local variables
0609 """
0610
0611 if callable(expr):
0612 if expr.__code__.co_argcount == 2:
0613 return expr
0614
0615 elif expr.__code__.co_argcount == 1:
0616 split = lambda z: (z.real, z.imag)
0617 output = lambda x, y: split(expr(x + y*1j))
0618 output.__name__ = expr.__name__
0619 return output
0620
0621 else:
0622 raise TypeError("must be a function of 2 or 1 variables")
0623
0624 if len(vars) == 2:
0625 g = math.__dict__
0626 if globals != None: g.update(globals)
0627 output = eval("lambda %s, %s: (%s)" % (vars[0], vars[1], expr), g, locals)
0628 output.__name__ = "%s,%s -> %s" % (vars[0], vars[1], expr)
0629 return output
0630
0631 elif len(vars) == 1:
0632 g = cmath.__dict__
0633 if globals != None: g.update(globals)
0634 output = eval("lambda %s: (%s)" % (vars[0], expr), g, locals)
0635 split = lambda z: (z.real, z.imag)
0636 output2 = lambda x, y: split(output(x + y*1j))
0637 output2.__name__ = "%s -> %s" % (vars[0], expr)
0638 return output2
0639
0640 else:
0641 raise TypeError("vars must have 2 or 1 elements")
0642
0643 def window(xmin, xmax, ymin, ymax, x=0, y=0, width=100, height=100, xlogbase=None, ylogbase=None, minusInfinity=-1000, flipx=False, flipy=True):
0644 """Creates and returns a coordinate transformation (a function that
0645 accepts two arguments and returns two values) that transforms from
0646 (xmin, ymin), (xmax, ymax)
0647 to
0648 (x, y), (x + width, y + height).
0649
0650 xlogbase, ylogbase default=None, None if a number, transform
0651 logarithmically with given base
0652 minusInfinity default=-1000 what to return if
0653 log(0 or negative) is attempted
0654 flipx default=False if true, reverse the direction of x
0655 flipy default=True if true, reverse the direction of y
0656
0657 (When composing windows, be sure to set flipy=False.)
0658 """
0659
0660 if flipx:
0661 ox1 = x + width
0662 ox2 = x
0663 else:
0664 ox1 = x
0665 ox2 = x + width
0666 if flipy:
0667 oy1 = y + height
0668 oy2 = y
0669 else:
0670 oy1 = y
0671 oy2 = y + height
0672 ix1 = xmin
0673 iy1 = ymin
0674 ix2 = xmax
0675 iy2 = ymax
0676
0677 if xlogbase != None and (ix1 <= 0. or ix2 <= 0.): raise ValueError("x range incompatible with log scaling: (%g, %g)" % (ix1, ix2))
0678
0679 if ylogbase != None and (iy1 <= 0. or iy2 <= 0.): raise ValueError("y range incompatible with log scaling: (%g, %g)" % (iy1, iy2))
0680
0681 def maybelog(t, it1, it2, ot1, ot2, logbase):
0682 if t <= 0.: return minusInfinity
0683 else:
0684 return ot1 + 1.*(math.log(t, logbase) - math.log(it1, logbase))/(math.log(it2, logbase) - math.log(it1, logbase)) * (ot2 - ot1)
0685
0686 xlogstr, ylogstr = "", ""
0687
0688 if xlogbase == None:
0689 xfunc = lambda x: ox1 + 1.*(x - ix1)/(ix2 - ix1) * (ox2 - ox1)
0690 else:
0691 xfunc = lambda x: maybelog(x, ix1, ix2, ox1, ox2, xlogbase)
0692 xlogstr = " xlog=%g" % xlogbase
0693
0694 if ylogbase == None:
0695 yfunc = lambda y: oy1 + 1.*(y - iy1)/(iy2 - iy1) * (oy2 - oy1)
0696 else:
0697 yfunc = lambda y: maybelog(y, iy1, iy2, oy1, oy2, ylogbase)
0698 ylogstr = " ylog=%g" % ylogbase
0699
0700 output = lambda x, y: (xfunc(x), yfunc(y))
0701
0702 output.__name__ = "(%g, %g), (%g, %g) -> (%g, %g), (%g, %g)%s%s" % (ix1, ix2, iy1, iy2, ox1, ox2, oy1, oy2, xlogstr, ylogstr)
0703 return output
0704
0705 def rotate(angle, cx=0, cy=0):
0706 """Creates and returns a coordinate transformation which rotates
0707 around (cx,cy) by "angle" degrees."""
0708 angle *= math.pi/180.
0709 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))
0710
0711 class Fig:
0712 """Stores graphics primitive objects and applies a single coordinate
0713 transformation to them. To compose coordinate systems, nest Fig
0714 objects.
0715
0716 Fig(obj, obj, obj..., trans=function)
0717
0718 obj optional list a list of drawing primatives
0719 trans default=None a coordinate transformation function
0720
0721 >>> fig = Fig(Line(0,0,1,1), Rect(0.2,0.2,0.8,0.8), trans="2*x, 2*y")
0722 >>> print fig.SVG().xml()
0723 <g>
0724 <path d='M0 0L2 2' />
0725 <path d='M0.4 0.4L1.6 0.4ZL1.6 1.6ZL0.4 1.6ZL0.4 0.4ZZ' />
0726 </g>
0727 >>> print Fig(fig, trans="x/2., y/2.").SVG().xml()
0728 <g>
0729 <path d='M0 0L1 1' />
0730 <path d='M0.2 0.2L0.8 0.2ZL0.8 0.8ZL0.2 0.8ZL0.2 0.2ZZ' />
0731 </g>
0732 """
0733
0734 def __repr__(self):
0735 if self.trans == None:
0736 return "<Fig (%d items)>" % len(self.d)
0737 elif isinstance(self.trans, str):
0738 return "<Fig (%d items) x,y -> %s>" % (len(self.d), self.trans)
0739 else:
0740 return "<Fig (%d items) %s>" % (len(self.d), self.trans.__name__)
0741
0742 def __init__(self, *d, **kwds):
0743 self.d = list(d)
0744 defaults = {"trans":None}
0745 defaults.update(kwds)
0746 kwds = defaults
0747
0748 self.trans = kwds["trans"]; del kwds["trans"]
0749 if len(kwds) != 0:
0750 raise TypeError("Fig() got unexpected keyword arguments %s" % kwds.keys())
0751
0752 def SVG(self, trans=None):
0753 """Apply the transformation "trans" and return an SVG object.
0754
0755 Coordinate transformations in nested Figs will be composed.
0756 """
0757
0758 if trans == None: trans = self.trans
0759 if isinstance(trans, str): trans = totrans(trans)
0760
0761 output = SVG("g")
0762 for s in self.d:
0763 if isinstance(s, SVG):
0764 output.append(s)
0765
0766 elif isinstance(s, Fig):
0767 strans = s.trans
0768 if isinstance(strans, str): strans = totrans(strans)
0769
0770 if trans == None: subtrans = strans
0771 elif strans == None: subtrans = trans
0772 else: subtrans = lambda x,y: trans(*strans(x, y))
0773
0774 output.sub += s.SVG(subtrans).sub
0775
0776 elif s == None: pass
0777
0778 else:
0779 output.append(s.SVG(trans))
0780
0781 return output
0782
0783 class Plot:
0784 """Acts like Fig, but draws a coordinate axis. You also need to supply plot ranges.
0785
0786 Plot(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
0787
0788 xmin, xmax required minimum and maximum x values (in the objs' coordinates)
0789 ymin, ymax required minimum and maximum y values (in the objs' coordinates)
0790 obj optional list drawing primatives
0791 keyword options keyword list options defined below
0792
0793 The following are keyword options, with their default values:
0794
0795 trans None transformation function
0796 x, y 5, 5 upper-left corner of the Plot in SVG coordinates
0797 width, height 90, 90 width and height of the Plot in SVG coordinates
0798 flipx, flipy False, True flip the sign of the coordinate axis
0799 minusInfinity -1000 if an axis is logarithmic and an object is plotted at 0 or
0800 a negative value, -1000 will be used as a stand-in for NaN
0801 atx, aty 0, 0 the place where the coordinate axes cross
0802 xticks -10 request ticks according to the standard tick specification
0803 (see help(Ticks))
0804 xminiticks True request miniticks according to the standard minitick
0805 specification
0806 xlabels True request tick labels according to the standard tick label
0807 specification
0808 xlogbase None if a number, the axis and transformation are logarithmic
0809 with ticks at the given base (10 being the most common)
0810 (same for y)
0811 arrows None if a new identifier, create arrow markers and draw them
0812 at the ends of the coordinate axes
0813 text_attr {} a dictionary of attributes for label text
0814 axis_attr {} a dictionary of attributes for the axis lines
0815 """
0816
0817 def __repr__(self):
0818 if self.trans == None:
0819 return "<Plot (%d items)>" % len(self.d)
0820 else:
0821 return "<Plot (%d items) %s>" % (len(self.d), self.trans.__name__)
0822
0823 def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
0824 self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
0825 self.d = list(d)
0826 defaults = {"trans":None, "x":5, "y":5, "width":90, "height":90, "flipx":False, "flipy":True, "minusInfinity":-1000, \
0827 "atx":0, "xticks":-10, "xminiticks":True, "xlabels":True, "xlogbase":None, \
0828 "aty":0, "yticks":-10, "yminiticks":True, "ylabels":True, "ylogbase":None, \
0829 "arrows":None, "text_attr":{}, "axis_attr":{}}
0830 defaults.update(kwds)
0831 kwds = defaults
0832
0833 self.trans = kwds["trans"]; del kwds["trans"]
0834 self.x = kwds["x"]; del kwds["x"]
0835 self.y = kwds["y"]; del kwds["y"]
0836 self.width = kwds["width"]; del kwds["width"]
0837 self.height = kwds["height"]; del kwds["height"]
0838 self.flipx = kwds["flipx"]; del kwds["flipx"]
0839 self.flipy = kwds["flipy"]; del kwds["flipy"]
0840 self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
0841 self.atx = kwds["atx"]; del kwds["atx"]
0842 self.xticks = kwds["xticks"]; del kwds["xticks"]
0843 self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
0844 self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
0845 self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
0846 self.aty = kwds["aty"]; del kwds["aty"]
0847 self.yticks = kwds["yticks"]; del kwds["yticks"]
0848 self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
0849 self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
0850 self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
0851 self.arrows = kwds["arrows"]; del kwds["arrows"]
0852 self.text_attr = kwds["text_attr"]; del kwds["text_attr"]
0853 self.axis_attr = kwds["axis_attr"]; del kwds["axis_attr"]
0854 if len(kwds) != 0:
0855 raise TypeError("Plot() got unexpected keyword arguments %s" % kwds.keys())
0856
0857 def SVG(self, trans=None):
0858 """Apply the transformation "trans" and return an SVG object."""
0859 if trans == None: trans = self.trans
0860 if isinstance(trans, str): trans = totrans(trans)
0861
0862 self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax, x=self.x, y=self.y, width=self.width, height=self.height, \
0863 xlogbase=self.xlogbase, ylogbase=self.ylogbase, minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
0864
0865 d = [Axes(self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty, \
0866 self.xticks, self.xminiticks, self.xlabels, self.xlogbase, \
0867 self.yticks, self.yminiticks, self.ylabels, self.ylogbase, \
0868 self.arrows, self.text_attr, **self.axis_attr)] \
0869 + self.d
0870
0871 return Fig(Fig(*d, **{"trans":trans})).SVG(self.last_window)
0872
0873 class Frame:
0874 text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
0875 axis_defaults = {}
0876
0877 tick_length = 1.5
0878 minitick_length = 0.75
0879 text_xaxis_offset = 1.
0880 text_yaxis_offset = 2.
0881 text_xtitle_offset = 6.
0882 text_ytitle_offset = 12.
0883
0884 def __repr__(self):
0885 return "<Frame (%d items)>" % len(self.d)
0886
0887 def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
0888 """Acts like Fig, but draws a coordinate frame around the data. You also need to supply plot ranges.
0889
0890 Frame(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
0891
0892 xmin, xmax required minimum and maximum x values (in the objs' coordinates)
0893 ymin, ymax required minimum and maximum y values (in the objs' coordinates)
0894 obj optional list drawing primatives
0895 keyword options keyword list options defined below
0896
0897 The following are keyword options, with their default values:
0898
0899 x, y 20, 5 upper-left corner of the Frame in SVG coordinates
0900 width, height 75, 80 width and height of the Frame in SVG coordinates
0901 flipx, flipy False, True flip the sign of the coordinate axis
0902 minusInfinity -1000 if an axis is logarithmic and an object is plotted at 0 or
0903 a negative value, -1000 will be used as a stand-in for NaN
0904 xtitle None if a string, label the x axis
0905 xticks -10 request ticks according to the standard tick specification
0906 (see help(Ticks))
0907 xminiticks True request miniticks according to the standard minitick
0908 specification
0909 xlabels True request tick labels according to the standard tick label
0910 specification
0911 xlogbase None if a number, the axis and transformation are logarithmic
0912 with ticks at the given base (10 being the most common)
0913 (same for y)
0914 text_attr {} a dictionary of attributes for label text
0915 axis_attr {} a dictionary of attributes for the axis lines
0916 """
0917
0918 self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
0919 self.d = list(d)
0920 defaults = {"x":20, "y":5, "width":75, "height":80, "flipx":False, "flipy":True, "minusInfinity":-1000, \
0921 "xtitle":None, "xticks":-10, "xminiticks":True, "xlabels":True, "x2labels":None, "xlogbase":None, \
0922 "ytitle":None, "yticks":-10, "yminiticks":True, "ylabels":True, "y2labels":None, "ylogbase":None, \
0923 "text_attr":{}, "axis_attr":{}}
0924 defaults.update(kwds)
0925 kwds = defaults
0926
0927 self.x = kwds["x"]; del kwds["x"]
0928 self.y = kwds["y"]; del kwds["y"]
0929 self.width = kwds["width"]; del kwds["width"]
0930 self.height = kwds["height"]; del kwds["height"]
0931 self.flipx = kwds["flipx"]; del kwds["flipx"]
0932 self.flipy = kwds["flipy"]; del kwds["flipy"]
0933 self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
0934 self.xtitle = kwds["xtitle"]; del kwds["xtitle"]
0935 self.xticks = kwds["xticks"]; del kwds["xticks"]
0936 self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
0937 self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
0938 self.x2labels = kwds["x2labels"]; del kwds["x2labels"]
0939 self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
0940 self.ytitle = kwds["ytitle"]; del kwds["ytitle"]
0941 self.yticks = kwds["yticks"]; del kwds["yticks"]
0942 self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
0943 self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
0944 self.y2labels = kwds["y2labels"]; del kwds["y2labels"]
0945 self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
0946
0947 self.text_attr = dict(self.text_defaults)
0948 self.text_attr.update(kwds["text_attr"]); del kwds["text_attr"]
0949
0950 self.axis_attr = dict(self.axis_defaults)
0951 self.axis_attr.update(kwds["axis_attr"]); del kwds["axis_attr"]
0952
0953 if len(kwds) != 0:
0954 raise TypeError("Frame() got unexpected keyword arguments %s" % kwds.keys())
0955
0956 def SVG(self):
0957 """Apply the window transformation and return an SVG object."""
0958
0959 self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax, x=self.x, y=self.y, width=self.width, height=self.height, \
0960 xlogbase=self.xlogbase, ylogbase=self.ylogbase, minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
0961
0962 left = YAxis(self.ymin, self.ymax, self.xmin, self.yticks, self.yminiticks, self.ylabels, self.ylogbase, None, None, None, self.text_attr, **self.axis_attr)
0963 right = YAxis(self.ymin, self.ymax, self.xmax, self.yticks, self.yminiticks, self.y2labels, self.ylogbase, None, None, None, self.text_attr, **self.axis_attr)
0964 bottom = XAxis(self.xmin, self.xmax, self.ymin, self.xticks, self.xminiticks, self.xlabels, self.xlogbase, None, None, None, self.text_attr, **self.axis_attr)
0965 top = XAxis(self.xmin, self.xmax, self.ymax, self.xticks, self.xminiticks, self.x2labels, self.xlogbase, None, None, None, self.text_attr, **self.axis_attr)
0966
0967 left.tick_start = -self.tick_length
0968 left.tick_end = 0
0969 left.minitick_start = -self.minitick_length
0970 left.minitick_end = 0.
0971 left.text_start = self.text_yaxis_offset
0972
0973 right.tick_start = 0.
0974 right.tick_end = self.tick_length
0975 right.minitick_start = 0.
0976 right.minitick_end = self.minitick_length
0977 right.text_start = -self.text_yaxis_offset
0978 right.text_attr["text-anchor"] = "start"
0979
0980 bottom.tick_start = 0.
0981 bottom.tick_end = self.tick_length
0982 bottom.minitick_start = 0.
0983 bottom.minitick_end = self.minitick_length
0984 bottom.text_start = -self.text_xaxis_offset
0985
0986 top.tick_start = -self.tick_length
0987 top.tick_end = 0.
0988 top.minitick_start = -self.minitick_length
0989 top.minitick_end = 0.
0990 top.text_start = self.text_xaxis_offset
0991 top.text_attr["dominant-baseline"] = "text-after-edge"
0992
0993 output = Fig(*self.d).SVG(self.last_window)
0994 output.prepend(left.SVG(self.last_window))
0995 output.prepend(bottom.SVG(self.last_window))
0996 output.prepend(right.SVG(self.last_window))
0997 output.prepend(top.SVG(self.last_window))
0998
0999 if self.xtitle != None:
1000 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))
1001 if self.ytitle != None:
1002 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))
1003 return output
1004
1005
1006
1007 def pathtoPath(svg):
1008 """Converts SVG("path", d="...") into Path(d=[...])."""
1009 if not isinstance(svg, SVG) or svg.t != "path":
1010 raise TypeError("Only SVG <path /> objects can be converted into Paths")
1011 attr = dict(svg.attr)
1012 d = attr["d"]
1013 del attr["d"]
1014 for key in attr.keys():
1015 if not isinstance(key, str):
1016 value = attr[key]
1017 del attr[key]
1018 attr[str(key)] = value
1019 return Path(d, **attr)
1020
1021 class Path:
1022 """Path represents an SVG path, an arbitrary set of curves and
1023 straight segments. Unlike SVG("path", d="..."), Path stores
1024 coordinates as a list of numbers, rather than a string, so that it is
1025 transformable in a Fig.
1026
1027 Path(d, attribute=value)
1028
1029 d required path data
1030 attribute=value pairs keyword list SVG attributes
1031
1032 See http://www.w3.org/TR/SVG/paths.html for specification of paths
1033 from text.
1034
1035 Internally, Path data is a list of tuples with these definitions:
1036
1037 * ("Z/z",): close the current path
1038 * ("H/h", x) or ("V/v", y): a horizontal or vertical line
1039 segment to x or y
1040 * ("M/m/L/l/T/t", x, y, global): moveto, lineto, or smooth
1041 quadratic curveto point (x, y). If global=True, (x, y) should
1042 not be transformed.
1043 * ("S/sQ/q", cx, cy, cglobal, x, y, global): polybezier or
1044 smooth quadratic curveto point (x, y) using (cx, cy) as a
1045 control point. If cglobal or global=True, (cx, cy) or (x, y)
1046 should not be transformed.
1047 * ("C/c", c1x, c1y, c1global, c2x, c2y, c2global, x, y, global):
1048 cubic curveto point (x, y) using (c1x, c1y) and (c2x, c2y) as
1049 control points. If c1global, c2global, or global=True, (c1x, c1y),
1050 (c2x, c2y), or (x, y) should not be transformed.
1051 * ("A/a", rx, ry, rglobal, x-axis-rotation, angle, large-arc-flag,
1052 sweep-flag, x, y, global): arcto point (x, y) using the
1053 aforementioned parameters.
1054 * (",/.", rx, ry, rglobal, angle, x, y, global): an ellipse at
1055 point (x, y) with radii (rx, ry). If angle is 0, the whole
1056 ellipse is drawn; otherwise, a partial ellipse is drawn.
1057 """
1058 defaults = {}
1059
1060 def __repr__(self):
1061 return "<Path (%d nodes) %s>" % (len(self.d), self.attr)
1062
1063 def __init__(self, d=[], **attr):
1064 if isinstance(d, str): self.d = self.parse(d)
1065 else: self.d = list(d)
1066
1067 self.attr = dict(self.defaults)
1068 self.attr.update(attr)
1069
1070 def parse_whitespace(self, index, pathdata):
1071 """Part of Path's text-command parsing algorithm; used internally."""
1072 while index < len(pathdata) and pathdata[index] in (" ", "\t", "\r", "\n", ","): index += 1
1073 return index, pathdata
1074
1075 def parse_command(self, index, pathdata):
1076 """Part of Path's text-command parsing algorithm; used internally."""
1077 index, pathdata = self.parse_whitespace(index, pathdata)
1078
1079 if index >= len(pathdata): return None, index, pathdata
1080 command = pathdata[index]
1081 if "A" <= command <= "Z" or "a" <= command <= "z":
1082 index += 1
1083 return command, index, pathdata
1084 else:
1085 return None, index, pathdata
1086
1087 def parse_number(self, index, pathdata):
1088 """Part of Path's text-command parsing algorithm; used internally."""
1089 index, pathdata = self.parse_whitespace(index, pathdata)
1090
1091 if index >= len(pathdata): return None, index, pathdata
1092 first_digit = pathdata[index]
1093
1094 if "0" <= first_digit <= "9" or first_digit in ("-", "+", "."):
1095 start = index
1096 while index < len(pathdata) and ("0" <= pathdata[index] <= "9" or pathdata[index] in ("-", "+", ".", "e", "E")):
1097 index += 1
1098 end = index
1099
1100 index = end
1101 return float(pathdata[start:end]), index, pathdata
1102 else:
1103 return None, index, pathdata
1104
1105 def parse_boolean(self, index, pathdata):
1106 """Part of Path's text-command parsing algorithm; used internally."""
1107 index, pathdata = self.parse_whitespace(index, pathdata)
1108
1109 if index >= len(pathdata): return None, index, pathdata
1110 first_digit = pathdata[index]
1111
1112 if first_digit in ("0", "1"):
1113 index += 1
1114 return int(first_digit), index, pathdata
1115 else:
1116 return None, index, pathdata
1117
1118 def parse(self, pathdata):
1119 """Parses text-commands, converting them into a list of tuples.
1120 Called by the constructor."""
1121 output = []
1122 index = 0
1123 while True:
1124 command, index, pathdata = self.parse_command(index, pathdata)
1125 index, pathdata = self.parse_whitespace(index, pathdata)
1126
1127 if command == None and index == len(pathdata): break
1128 if command in ("Z", "z"):
1129 output.append((command,))
1130
1131
1132 elif command in ("H", "h", "V", "v"):
1133 errstring = "Path command \"%s\" requires a number at index %d" % (command, index)
1134 num1, index, pathdata = self.parse_number(index, pathdata)
1135 if num1 == None: raise ValueError(errstring)
1136
1137 while num1 != None:
1138 output.append((command, num1))
1139 num1, index, pathdata = self.parse_number(index, pathdata)
1140
1141
1142 elif command in ("M", "m", "L", "l", "T", "t"):
1143 errstring = "Path command \"%s\" requires an x,y pair at index %d" % (command, index)
1144 num1, index, pathdata = self.parse_number(index, pathdata)
1145 num2, index, pathdata = self.parse_number(index, pathdata)
1146
1147 if num1 == None: raise ValueError(errstring)
1148
1149 while num1 != None:
1150 if num2 == None: raise ValueError(errstring)
1151 output.append((command, num1, num2, False))
1152
1153 num1, index, pathdata = self.parse_number(index, pathdata)
1154 num2, index, pathdata = self.parse_number(index, pathdata)
1155
1156
1157 elif command in ("S", "s", "Q", "q"):
1158 errstring = "Path command \"%s\" requires a cx,cy,x,y quadruplet at index %d" % (command, index)
1159 num1, index, pathdata = self.parse_number(index, pathdata)
1160 num2, index, pathdata = self.parse_number(index, pathdata)
1161 num3, index, pathdata = self.parse_number(index, pathdata)
1162 num4, index, pathdata = self.parse_number(index, pathdata)
1163
1164 if num1 == None: raise ValueError(errstring)
1165
1166 while num1 != None:
1167 if num2 == None or num3 == None or num4 == None: raise ValueError(errstring)
1168 output.append((command, num1, num2, False, num3, num4, False))
1169
1170 num1, index, pathdata = self.parse_number(index, pathdata)
1171 num2, index, pathdata = self.parse_number(index, pathdata)
1172 num3, index, pathdata = self.parse_number(index, pathdata)
1173 num4, index, pathdata = self.parse_number(index, pathdata)
1174
1175
1176 elif command in ("C", "c"):
1177 errstring = "Path command \"%s\" requires a c1x,c1y,c2x,c2y,x,y sextuplet at index %d" % (command, index)
1178 num1, index, pathdata = self.parse_number(index, pathdata)
1179 num2, index, pathdata = self.parse_number(index, pathdata)
1180 num3, index, pathdata = self.parse_number(index, pathdata)
1181 num4, index, pathdata = self.parse_number(index, pathdata)
1182 num5, index, pathdata = self.parse_number(index, pathdata)
1183 num6, index, pathdata = self.parse_number(index, pathdata)
1184
1185 if num1 == None: raise ValueError(errstring)
1186
1187 while num1 != None:
1188 if num2 == None or num3 == None or num4 == None or num5 == None or num6 == None: raise ValueError(errstring)
1189
1190 output.append((command, num1, num2, False, num3, num4, False, num5, num6, False))
1191
1192 num1, index, pathdata = self.parse_number(index, pathdata)
1193 num2, index, pathdata = self.parse_number(index, pathdata)
1194 num3, index, pathdata = self.parse_number(index, pathdata)
1195 num4, index, pathdata = self.parse_number(index, pathdata)
1196 num5, index, pathdata = self.parse_number(index, pathdata)
1197 num6, index, pathdata = self.parse_number(index, pathdata)
1198
1199
1200 elif command in ("A", "a"):
1201 errstring = "Path command \"%s\" requires a rx,ry,angle,large-arc-flag,sweep-flag,x,y septuplet at index %d" % (command, index)
1202 num1, index, pathdata = self.parse_number(index, pathdata)
1203 num2, index, pathdata = self.parse_number(index, pathdata)
1204 num3, index, pathdata = self.parse_number(index, pathdata)
1205 num4, index, pathdata = self.parse_boolean(index, pathdata)
1206 num5, index, pathdata = self.parse_boolean(index, pathdata)
1207 num6, index, pathdata = self.parse_number(index, pathdata)
1208 num7, index, pathdata = self.parse_number(index, pathdata)
1209
1210 if num1 == None: raise ValueError(errstring)
1211
1212 while num1 != None:
1213 if num2 == None or num3 == None or num4 == None or num5 == None or num6 == None or num7 == None: raise ValueError(errstring)
1214
1215 output.append((command, num1, num2, False, num3, num4, num5, num6, num7, False))
1216
1217 num1, index, pathdata = self.parse_number(index, pathdata)
1218 num2, index, pathdata = self.parse_number(index, pathdata)
1219 num3, index, pathdata = self.parse_number(index, pathdata)
1220 num4, index, pathdata = self.parse_boolean(index, pathdata)
1221 num5, index, pathdata = self.parse_boolean(index, pathdata)
1222 num6, index, pathdata = self.parse_number(index, pathdata)
1223 num7, index, pathdata = self.parse_number(index, pathdata)
1224
1225 return output
1226
1227 def SVG(self, trans=None):
1228 """Apply the transformation "trans" and return an SVG object."""
1229 if isinstance(trans, str): trans = totrans(trans)
1230
1231 x, y, X, Y = None, None, None, None
1232 output = []
1233 for datum in self.d:
1234 if not isinstance(datum, (tuple, list)):
1235 raise TypeError("pathdata elements must be tuples/lists")
1236
1237 command = datum[0]
1238
1239
1240 if command in ("Z", "z"):
1241 x, y, X, Y = None, None, None, None
1242 output.append("Z")
1243
1244
1245 elif command in ("H", "h", "V", "v"):
1246 command, num1 = datum
1247
1248 if command == "H" or (command == "h" and x == None): x = num1
1249 elif command == "h": x += num1
1250 elif command == "V" or (command == "v" and y == None): y = num1
1251 elif command == "v": y += num1
1252
1253 if trans == None: X, Y = x, y
1254 else: X, Y = trans(x, y)
1255
1256 output.append("L%g %g" % (X, Y))
1257
1258
1259 elif command in ("M", "m", "L", "l", "T", "t"):
1260 command, num1, num2, isglobal12 = datum
1261
1262 if trans == None or isglobal12:
1263 if command.isupper() or X == None or Y == None:
1264 X, Y = num1, num2
1265 else:
1266 X += num1
1267 Y += num2
1268 x, y = X, Y
1269
1270 else:
1271 if command.isupper() or x == None or y == None:
1272 x, y = num1, num2
1273 else:
1274 x += num1
1275 y += num2
1276 X, Y = trans(x, y)
1277
1278 COMMAND = command.capitalize()
1279 output.append("%s%g %g" % (COMMAND, X, Y))
1280
1281
1282 elif command in ("S", "s", "Q", "q"):
1283 command, num1, num2, isglobal12, num3, num4, isglobal34 = datum
1284
1285 if trans == None or isglobal12:
1286 if command.isupper() or X == None or Y == None:
1287 CX, CY = num1, num2
1288 else:
1289 CX = X + num1
1290 CY = Y + num2
1291
1292 else:
1293 if command.isupper() or x == None or y == None:
1294 cx, cy = num1, num2
1295 else:
1296 cx = x + num1
1297 cy = y + num2
1298 CX, CY = trans(cx, cy)
1299
1300 if trans == None or isglobal34:
1301 if command.isupper() or X == None or Y == None:
1302 X, Y = num3, num4
1303 else:
1304 X += num3
1305 Y += num4
1306 x, y = X, Y
1307
1308 else:
1309 if command.isupper() or x == None or y == None:
1310 x, y = num3, num4
1311 else:
1312 x += num3
1313 y += num4
1314 X, Y = trans(x, y)
1315
1316 COMMAND = command.capitalize()
1317 output.append("%s%g %g %g %g" % (COMMAND, CX, CY, X, Y))
1318
1319
1320 elif command in ("C", "c"):
1321 command, num1, num2, isglobal12, num3, num4, isglobal34, num5, num6, isglobal56 = datum
1322
1323 if trans == None or isglobal12:
1324 if command.isupper() or X == None or Y == None:
1325 C1X, C1Y = num1, num2
1326 else:
1327 C1X = X + num1
1328 C1Y = Y + num2
1329
1330 else:
1331 if command.isupper() or x == None or y == None:
1332 c1x, c1y = num1, num2
1333 else:
1334 c1x = x + num1
1335 c1y = y + num2
1336 C1X, C1Y = trans(c1x, c1y)
1337
1338 if trans == None or isglobal34:
1339 if command.isupper() or X == None or Y == None:
1340 C2X, C2Y = num3, num4
1341 else:
1342 C2X = X + num3
1343 C2Y = Y + num4
1344
1345 else:
1346 if command.isupper() or x == None or y == None:
1347 c2x, c2y = num3, num4
1348 else:
1349 c2x = x + num3
1350 c2y = y + num4
1351 C2X, C2Y = trans(c2x, c2y)
1352
1353 if trans == None or isglobal56:
1354 if command.isupper() or X == None or Y == None:
1355 X, Y = num5, num6
1356 else:
1357 X += num5
1358 Y += num6
1359 x, y = X, Y
1360
1361 else:
1362 if command.isupper() or x == None or y == None:
1363 x, y = num5, num6
1364 else:
1365 x += num5
1366 y += num6
1367 X, Y = trans(x, y)
1368
1369 COMMAND = command.capitalize()
1370 output.append("%s%g %g %g %g %g %g" % (COMMAND, C1X, C1Y, C2X, C2Y, X, Y))
1371
1372
1373 elif command in ("A", "a"):
1374 command, num1, num2, isglobal12, angle, large_arc_flag, sweep_flag, num3, num4, isglobal34 = datum
1375
1376 oldx, oldy = x, y
1377 OLDX, OLDY = X, Y
1378
1379 if trans == None or isglobal34:
1380 if command.isupper() or X == None or Y == None:
1381 X, Y = num3, num4
1382 else:
1383 X += num3
1384 Y += num4
1385 x, y = X, Y
1386
1387 else:
1388 if command.isupper() or x == None or y == None:
1389 x, y = num3, num4
1390 else:
1391 x += num3
1392 y += num4
1393 X, Y = trans(x, y)
1394
1395 if x != None and y != None:
1396 centerx, centery = (x + oldx)/2., (y + oldy)/2.
1397 CENTERX, CENTERY = (X + OLDX)/2., (Y + OLDY)/2.
1398
1399 if trans == None or isglobal12:
1400 RX = CENTERX + num1
1401 RY = CENTERY + num2
1402
1403 else:
1404 rx = centerx + num1
1405 ry = centery + num2
1406 RX, RY = trans(rx, ry)
1407
1408 COMMAND = command.capitalize()
1409 output.append("%s%g %g %g %d %d %g %g" % (COMMAND, RX - CENTERX, RY - CENTERY, angle, large_arc_flag, sweep_flag, X, Y))
1410
1411 elif command in (",", "."):
1412 command, num1, num2, isglobal12, angle, num3, num4, isglobal34 = datum
1413 if trans == None or isglobal34:
1414 if command == "." or X == None or Y == None:
1415 X, Y = num3, num4
1416 else:
1417 X += num3
1418 Y += num4
1419 x, y = None, None
1420
1421 else:
1422 if command == "." or x == None or y == None:
1423 x, y = num3, num4
1424 else:
1425 x += num3
1426 y += num4
1427 X, Y = trans(x, y)
1428
1429 if trans == None or isglobal12:
1430 RX = X + num1
1431 RY = Y + num2
1432
1433 else:
1434 rx = x + num1
1435 ry = y + num2
1436 RX, RY = trans(rx, ry)
1437
1438 RX, RY = RX - X, RY - Y
1439
1440 X1, Y1 = X + RX * math.cos(angle*math.pi/180.), Y + RX * math.sin(angle*math.pi/180.)
1441 X2, Y2 = X + RY * math.sin(angle*math.pi/180.), Y - RY * math.cos(angle*math.pi/180.)
1442 X3, Y3 = X - RX * math.cos(angle*math.pi/180.), Y - RX * math.sin(angle*math.pi/180.)
1443 X4, Y4 = X - RY * math.sin(angle*math.pi/180.), Y + RY * math.cos(angle*math.pi/180.)
1444
1445 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" \
1446 % (X1, Y1, RX, RY, angle, X2, Y2, RX, RY, angle, X3, Y3, RX, RY, angle, X4, Y4, RX, RY, angle, X1, Y1))
1447
1448 return SVG("path", d="".join(output), **self.attr)
1449
1450
1451
1452 def funcRtoC(expr, var="t", globals=None, locals=None):
1453 """Converts a complex "z(t)" string to a function acceptable for Curve.
1454
1455 expr required string in the form "z(t)"
1456 var default="t" name of the independent variable
1457 globals default=None dict of global variables used in the expression;
1458 you may want to use Python's builtin globals()
1459 locals default=None dict of local variables
1460 """
1461 g = cmath.__dict__
1462 if globals != None: g.update(globals)
1463 output = eval("lambda %s: (%s)" % (var, expr), g, locals)
1464 split = lambda z: (z.real, z.imag)
1465 output2 = lambda t: split(output(t))
1466 output2.__name__ = "%s -> %s" % (var, expr)
1467 return output2
1468
1469 def funcRtoR2(expr, var="t", globals=None, locals=None):
1470 """Converts a "f(t), g(t)" string to a function acceptable for Curve.
1471
1472 expr required string in the form "f(t), g(t)"
1473 var default="t" name of the independent variable
1474 globals default=None dict of global variables used in the expression;
1475 you may want to use Python's builtin globals()
1476 locals default=None dict of local variables
1477 """
1478 g = math.__dict__
1479 if globals != None: g.update(globals)
1480 output = eval("lambda %s: (%s)" % (var, expr), g, locals)
1481 output.__name__ = "%s -> %s" % (var, expr)
1482 return output
1483
1484 def funcRtoR(expr, var="x", globals=None, locals=None):
1485 """Converts a "f(x)" string to a function acceptable for Curve.
1486
1487 expr required string in the form "f(x)"
1488 var default="x" name of the independent variable
1489 globals default=None dict of global variables used in the expression;
1490 you may want to use Python's builtin globals()
1491 locals default=None dict of local variables
1492 """
1493 g = math.__dict__
1494 if globals != None: g.update(globals)
1495 output = eval("lambda %s: (%s, %s)" % (var, var, expr), g, locals)
1496 output.__name__ = "%s -> %s" % (var, expr)
1497 return output
1498
1499 class Curve:
1500 """Draws a parametric function as a path.
1501
1502 Curve(f, low, high, loop, attribute=value)
1503
1504 f required a Python callable or string in
1505 the form "f(t), g(t)"
1506 low, high required left and right endpoints
1507 loop default=False if True, connect the endpoints
1508 attribute=value pairs keyword list SVG attributes
1509 """
1510 defaults = {}
1511 random_sampling = True
1512 recursion_limit = 15
1513 linearity_limit = 0.05
1514 discontinuity_limit = 5.
1515
1516 def __repr__(self):
1517 return "<Curve %s [%s, %s] %s>" % (self.f, self.low, self.high, self.attr)
1518
1519 def __init__(self, f, low, high, loop=False, **attr):
1520 self.f = f
1521 self.low = low
1522 self.high = high
1523 self.loop = loop
1524
1525 self.attr = dict(self.defaults)
1526 self.attr.update(attr)
1527
1528
1529 class Sample:
1530 def __repr__(self):
1531 t, x, y, X, Y = self.t, self.x, self.y, self.X, self.Y
1532 if t != None: t = "%g" % t
1533 if x != None: x = "%g" % x
1534 if y != None: y = "%g" % y
1535 if X != None: X = "%g" % X
1536 if Y != None: Y = "%g" % Y
1537 return "<Curve.Sample t=%s x=%s y=%s X=%s Y=%s>" % (t, x, y, X, Y)
1538
1539 def __init__(self, t): self.t = t
1540
1541 def link(self, left, right): self.left, self.right = left, right
1542
1543 def evaluate(self, f, trans):
1544 self.x, self.y = f(self.t)
1545 if trans == None:
1546 self.X, self.Y = self.x, self.y
1547 else:
1548 self.X, self.Y = trans(self.x, self.y)
1549
1550
1551
1552 class Samples:
1553 def __repr__(self): return "<Curve.Samples (%d samples)>" % len(self)
1554
1555 def __init__(self, left, right): self.left, self.right = left, right
1556
1557 def __len__(self):
1558 count = 0
1559 current = self.left
1560 while current != None:
1561 count += 1
1562 current = current.right
1563 return count
1564
1565 def __iter__(self):
1566 self.current = self.left
1567 return self
1568
1569 def next(self):
1570 current = self.current
1571 if current == None: raise StopIteration
1572 self.current = self.current.right
1573 return current
1574
1575
1576 def sample(self, trans=None):
1577 """Adaptive-sampling algorithm that chooses the best sample points
1578 for a parametric curve between two endpoints and detects
1579 discontinuities. Called by SVG()."""
1580 oldrecursionlimit = sys.getrecursionlimit()
1581 sys.setrecursionlimit(self.recursion_limit + 100)
1582 try:
1583
1584 if not (self.low < self.high): raise ValueError("low must be less than high")
1585 low, high = self.Sample(float(self.low)), self.Sample(float(self.high))
1586 low.link(None, high)
1587 high.link(low, None)
1588
1589 low.evaluate(self.f, trans)
1590 high.evaluate(self.f, trans)
1591
1592
1593 self.subsample(low, high, 0, trans)
1594
1595
1596 left = low
1597 while left.right != None:
1598
1599 mid = left.right
1600 right = mid.right
1601 if right != None and left.X != None and left.Y != None and mid.X != None and mid.Y != None and right.X != None and right.Y != None:
1602 numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
1603 denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
1604 if denom != 0. and abs(numer/denom) < self.linearity_limit:
1605
1606 left.right = right
1607 right.left = left
1608 else:
1609
1610 left = left.right
1611 else:
1612 left = left.right
1613
1614 self.last_samples = self.Samples(low, high)
1615
1616 finally:
1617 sys.setrecursionlimit(oldrecursionlimit)
1618
1619 def subsample(self, left, right, depth, trans=None):
1620 """Part of the adaptive-sampling algorithm that chooses the best
1621 sample points. Called by sample()."""
1622
1623 if self.random_sampling:
1624 mid = self.Sample(left.t + random.uniform(0.3, 0.7) * (right.t - left.t))
1625 else:
1626 mid = self.Sample(left.t + 0.5 * (right.t - left.t))
1627
1628 left.right = mid
1629 right.left = mid
1630 mid.link(left, right)
1631 mid.evaluate(self.f, trans)
1632
1633
1634 numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
1635 denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
1636
1637
1638 if depth < 3 or (denom == 0 and left.t != right.t) or denom > self.discontinuity_limit or (denom != 0. and abs(numer/denom) > self.linearity_limit):
1639
1640
1641 if depth < self.recursion_limit:
1642 self.subsample(left, mid, depth+1, trans)
1643 self.subsample(mid, right, depth+1, trans)
1644
1645 else:
1646
1647
1648 mid.y = mid.Y = None
1649
1650 def SVG(self, trans=None):
1651 """Apply the transformation "trans" and return an SVG object."""
1652 return self.Path(trans).SVG()
1653
1654 def Path(self, trans=None, local=False):
1655 """Apply the transformation "trans" and return a Path object in
1656 global coordinates. If local=True, return a Path in local coordinates
1657 (which must be transformed again)."""
1658
1659 if isinstance(trans, str): trans = totrans(trans)
1660 if isinstance(self.f, str): self.f = funcRtoR2(self.f)
1661
1662 self.sample(trans)
1663
1664 output = []
1665 for s in self.last_samples:
1666 if s.X != None and s.Y != None:
1667 if s.left == None or s.left.Y == None:
1668 command = "M"
1669 else:
1670 command = "L"
1671
1672 if local: output.append((command, s.x, s.y, False))
1673 else: output.append((command, s.X, s.Y, True))
1674
1675 if self.loop: output.append(("Z",))
1676 return Path(output, **self.attr)
1677
1678
1679
1680 class Poly:
1681 """Draws a curve specified by a sequence of points. The curve may be
1682 piecewise linear, like a polygon, or a Bezier curve.
1683
1684 Poly(d, mode, loop, attribute=value)
1685
1686 d required list of tuples representing points
1687 and possibly control points
1688 mode default="L" "lines", "bezier", "velocity",
1689 "foreback", "smooth", or an abbreviation
1690 loop default=False if True, connect the first and last
1691 point, closing the loop
1692 attribute=value pairs keyword list SVG attributes
1693
1694 The format of the tuples in d depends on the mode.
1695
1696 "lines"/"L" d=[(x,y), (x,y), ...]
1697 piecewise-linear segments joining the (x,y) points
1698 "bezier"/"B" d=[(x, y, c1x, c1y, c2x, c2y), ...]
1699 Bezier curve with two control points (control points
1700 preceed (x,y), as in SVG paths). If (c1x,c1y) and
1701 (c2x,c2y) both equal (x,y), you get a linear
1702 interpolation ("lines")
1703 "velocity"/"V" d=[(x, y, vx, vy), ...]
1704 curve that passes through (x,y) with velocity (vx,vy)
1705 (one unit of arclength per unit time); in other words,
1706 (vx,vy) is the tangent vector at (x,y). If (vx,vy) is
1707 (0,0), you get a linear interpolation ("lines").
1708 "foreback"/"F" d=[(x, y, bx, by, fx, fy), ...]
1709 like "velocity" except that there is a left derivative
1710 (bx,by) and a right derivative (fx,fy). If (bx,by)
1711 equals (fx,fy) (with no minus sign), you get a
1712 "velocity" curve
1713 "smooth"/"S" d=[(x,y), (x,y), ...]
1714 a "velocity" interpolation with (vx,vy)[i] equal to
1715 ((x,y)[i+1] - (x,y)[i-1])/2: the minimal derivative
1716 """
1717 defaults = {}
1718
1719 def __repr__(self):
1720 return "<Poly (%d nodes) mode=%s loop=%s %s>" % (len(self.d), self.mode, repr(self.loop), self.attr)
1721
1722 def __init__(self, d=[], mode="L", loop=False, **attr):
1723 self.d = list(d)
1724 self.mode = mode
1725 self.loop = loop
1726
1727 self.attr = dict(self.defaults)
1728 self.attr.update(attr)
1729
1730 def SVG(self, trans=None):
1731 """Apply the transformation "trans" and return an SVG object."""
1732 return self.Path(trans).SVG()
1733
1734 def Path(self, trans=None, local=False):
1735 """Apply the transformation "trans" and return a Path object in
1736 global coordinates. If local=True, return a Path in local coordinates
1737 (which must be transformed again)."""
1738 if isinstance(trans, str): trans = totrans(trans)
1739
1740 if self.mode[0] == "L" or self.mode[0] == "l": mode = "L"
1741 elif self.mode[0] == "B" or self.mode[0] == "b": mode = "B"
1742 elif self.mode[0] == "V" or self.mode[0] == "v": mode = "V"
1743 elif self.mode[0] == "F" or self.mode[0] == "f": mode = "F"
1744 elif self.mode[0] == "S" or self.mode[0] == "s":
1745 mode = "S"
1746
1747 vx, vy = [0.]*len(self.d), [0.]*len(self.d)
1748 for i in range(len(self.d)):
1749 inext = (i+1) % len(self.d)
1750 iprev = (i-1) % len(self.d)
1751
1752 vx[i] = (self.d[inext][0] - self.d[iprev][0])/2.
1753 vy[i] = (self.d[inext][1] - self.d[iprev][1])/2.
1754 if not self.loop and (i == 0 or i == len(self.d)-1):
1755 vx[i], vy[i] = 0., 0.
1756
1757 else:
1758 raise ValueError("mode must be \"lines\", \"bezier\", \"velocity\", \"foreback\", \"smooth\", or an abbreviation")
1759
1760 d = []
1761 indexes = list(range(len(self.d)))
1762 if self.loop and len(self.d) > 0: indexes.append(0)
1763
1764 for i in indexes:
1765 inext = (i+1) % len(self.d)
1766 iprev = (i-1) % len(self.d)
1767
1768 x, y = self.d[i][0], self.d[i][1]
1769
1770 if trans == None: X, Y = x, y
1771 else: X, Y = trans(x, y)
1772
1773 if d == []:
1774 if local: d.append(("M", x, y, False))
1775 else: d.append(("M", X, Y, True))
1776
1777 elif mode == "L":
1778 if local: d.append(("L", x, y, False))
1779 else: d.append(("L", X, Y, True))
1780
1781 elif mode == "B":
1782 c1x, c1y = self.d[i][2], self.d[i][3]
1783 if trans == None: C1X, C1Y = c1x, c1y
1784 else: C1X, C1Y = trans(c1x, c1y)
1785
1786 c2x, c2y = self.d[i][4], self.d[i][5]
1787 if trans == None: C2X, C2Y = c2x, c2y
1788 else: C2X, C2Y = trans(c2x, c2y)
1789
1790 if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1791 else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1792
1793 elif mode == "V":
1794 c1x, c1y = self.d[iprev][2]/3. + self.d[iprev][0], self.d[iprev][3]/3. + self.d[iprev][1]
1795 c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
1796
1797 if trans == None: C1X, C1Y = c1x, c1y
1798 else: C1X, C1Y = trans(c1x, c1y)
1799 if trans == None: C2X, C2Y = c2x, c2y
1800 else: C2X, C2Y = trans(c2x, c2y)
1801
1802 if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1803 else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1804
1805 elif mode == "F":
1806 c1x, c1y = self.d[iprev][4]/3. + self.d[iprev][0], self.d[iprev][5]/3. + self.d[iprev][1]
1807 c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
1808
1809 if trans == None: C1X, C1Y = c1x, c1y
1810 else: C1X, C1Y = trans(c1x, c1y)
1811 if trans == None: C2X, C2Y = c2x, c2y
1812 else: C2X, C2Y = trans(c2x, c2y)
1813
1814 if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1815 else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1816
1817 elif mode == "S":
1818 c1x, c1y = vx[iprev]/3. + self.d[iprev][0], vy[iprev]/3. + self.d[iprev][1]
1819 c2x, c2y = vx[i]/-3. + x, vy[i]/-3. + y
1820
1821 if trans == None: C1X, C1Y = c1x, c1y
1822 else: C1X, C1Y = trans(c1x, c1y)
1823 if trans == None: C2X, C2Y = c2x, c2y
1824 else: C2X, C2Y = trans(c2x, c2y)
1825
1826 if local: d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
1827 else: d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
1828
1829 if self.loop and len(self.d) > 0: d.append(("Z",))
1830
1831 return Path(d, **self.attr)
1832
1833
1834
1835 class Text:
1836 """Draws at text string at a specified point in local coordinates.
1837
1838 x, y required location of the point in local coordinates
1839 d required text/Unicode string
1840 attribute=value pairs keyword list SVG attributes
1841 """
1842
1843 defaults = {"stroke":"none", "fill":"black", "font-size":5}
1844
1845 def __repr__(self):
1846 return "<Text %s at (%g, %g) %s>" % (repr(self.d), self.x, self.y, self.attr)
1847
1848 def __init__(self, x, y, d, **attr):
1849 self.x = x
1850 self.y = y
1851 self.d = str(d)
1852 self.attr = dict(self.defaults)
1853 self.attr.update(attr)
1854
1855 def SVG(self, trans=None):
1856 """Apply the transformation "trans" and return an SVG object."""
1857 if isinstance(trans, str): trans = totrans(trans)
1858
1859 X, Y = self.x, self.y
1860 if trans != None: X, Y = trans(X, Y)
1861 return SVG("text", self.d, x=X, y=Y, **self.attr)
1862
1863 class TextGlobal:
1864 """Draws at text string at a specified point in global coordinates.
1865
1866 x, y required location of the point in global coordinates
1867 d required text/Unicode string
1868 attribute=value pairs keyword list SVG attributes
1869 """
1870 defaults = {"stroke":"none", "fill":"black", "font-size":5}
1871
1872 def __repr__(self):
1873 return "<TextGlobal %s at (%s, %s) %s>" % (repr(self.d), str(self.x), str(self.y), self.attr)
1874
1875 def __init__(self, x, y, d, **attr):
1876 self.x = x
1877 self.y = y
1878 self.d = str(d)
1879 self.attr = dict(self.defaults)
1880 self.attr.update(attr)
1881
1882 def SVG(self, trans=None):
1883 """Apply the transformation "trans" and return an SVG object."""
1884 return SVG("text", self.d, x=self.x, y=self.y, **self.attr)
1885
1886
1887
1888 _symbol_templates = {"dot": SVG("symbol", SVG("circle", cx=0, cy=0, r=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), \
1889 "box": SVG("symbol", SVG("rect", x1=-1, y1=-1, x2=1, y2=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"), \
1890 "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"), \
1891 "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"), \
1892 }
1893
1894 def make_symbol(id, shape="dot", **attr):
1895 """Creates a new instance of an SVG symbol to avoid cross-linking objects.
1896
1897 id required a new identifier (string/Unicode)
1898 shape default="dot" the shape name from _symbol_templates
1899 attribute=value list keyword list modify the SVG attributes of the new symbol
1900 """
1901 output = copy.deepcopy(_symbol_templates[shape])
1902 for i in output.sub: i.attr.update(attr_preprocess(attr))
1903 output["id"] = id
1904 return output
1905
1906 _circular_dot = make_symbol("circular_dot")
1907
1908 class Dots:
1909 """Dots draws SVG symbols at a set of points.
1910
1911 d required list of (x,y) points
1912 symbol default=None SVG symbol or a new identifier to
1913 label an auto-generated symbol;
1914 if None, use pre-defined _circular_dot
1915 width, height default=1, 1 width and height of the symbols
1916 in SVG coordinates
1917 attribute=value pairs keyword list SVG attributes
1918 """
1919 defaults = {}
1920
1921 def __repr__(self):
1922 return "<Dots (%d nodes) %s>" % (len(self.d), self.attr)
1923
1924 def __init__(self, d=[], symbol=None, width=1., height=1., **attr):
1925 self.d = list(d)
1926 self.width = width
1927 self.height = height
1928
1929 self.attr = dict(self.defaults)
1930 self.attr.update(attr)
1931
1932 if symbol == None:
1933 self.symbol = _circular_dot
1934 elif isinstance(symbol, SVG):
1935 self.symbol = symbol
1936 else:
1937 self.symbol = make_symbol(symbol)
1938
1939 def SVG(self, trans=None):
1940 """Apply the transformation "trans" and return an SVG object."""
1941 if isinstance(trans, str): trans = totrans(trans)
1942
1943 output = SVG("g", SVG("defs", self.symbol))
1944 id = "#%s" % self.symbol["id"]
1945
1946 for p in self.d:
1947 x, y = p[0], p[1]
1948
1949 if trans == None: X, Y = x, y
1950 else: X, Y = trans(x, y)
1951
1952 item = SVG("use", x=X, y=Y, xlink__href=id)
1953 if self.width != None: item["width"] = self.width
1954 if self.height != None: item["height"] = self.height
1955 output.append(item)
1956
1957 return output
1958
1959
1960
1961 _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"), \
1962 "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"), \
1963 }
1964
1965 def make_marker(id, shape, **attr):
1966 """Creates a new instance of an SVG marker to avoid cross-linking objects.
1967
1968 id required a new identifier (string/Unicode)
1969 shape required the shape name from _marker_templates
1970 attribute=value list keyword list modify the SVG attributes of the new marker
1971 """
1972 output = copy.deepcopy(_marker_templates[shape])
1973 for i in output.sub: i.attr.update(attr_preprocess(attr))
1974 output["id"] = id
1975 return output
1976
1977 class Line(Curve):
1978 """Draws a line between two points.
1979
1980 Line(x1, y1, x2, y2, arrow_start, arrow_end, attribute=value)
1981
1982 x1, y1 required the starting point
1983 x2, y2 required the ending point
1984 arrow_start default=None if an identifier string/Unicode,
1985 draw a new arrow object at the
1986 beginning of the line; if a marker,
1987 draw that marker instead
1988 arrow_end default=None same for the end of the line
1989 attribute=value pairs keyword list SVG attributes
1990 """
1991 defaults = {}
1992
1993 def __repr__(self):
1994 return "<Line (%g, %g) to (%g, %g) %s>" % (self.x1, self.y1, self.x2, self.y2, self.attr)
1995
1996 def __init__(self, x1, y1, x2, y2, arrow_start=None, arrow_end=None, **attr):
1997 self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
1998 self.arrow_start, self.arrow_end = arrow_start, arrow_end
1999
2000 self.attr = dict(self.defaults)
2001 self.attr.update(attr)
2002
2003 def SVG(self, trans=None):
2004 """Apply the transformation "trans" and return an SVG object."""
2005
2006 line = self.Path(trans).SVG()
2007
2008 if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
2009 defs = SVG("defs")
2010
2011 if self.arrow_start != False and self.arrow_start != None:
2012 if isinstance(self.arrow_start, SVG):
2013 defs.append(self.arrow_start)
2014 line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
2015 elif isinstance(self.arrow_start, str):
2016 defs.append(make_marker(self.arrow_start, "arrow_start"))
2017 line.attr["marker-start"] = "url(#%s)" % self.arrow_start
2018 else:
2019 raise TypeError("arrow_start must be False/None or an id string for the new marker")
2020
2021 if self.arrow_end != False and self.arrow_end != None:
2022 if isinstance(self.arrow_end, SVG):
2023 defs.append(self.arrow_end)
2024 line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
2025 elif isinstance(self.arrow_end, str):
2026 defs.append(make_marker(self.arrow_end, "arrow_end"))
2027 line.attr["marker-end"] = "url(#%s)" % self.arrow_end
2028 else:
2029 raise TypeError("arrow_end must be False/None or an id string for the new marker")
2030
2031 return SVG("g", defs, line)
2032
2033 return line
2034
2035 def Path(self, trans=None, local=False):
2036 """Apply the transformation "trans" and return a Path object in
2037 global coordinates. If local=True, return a Path in local coordinates
2038 (which must be transformed again)."""
2039 self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1 + t*(self.y2 - self.y1))
2040 self.low = 0.
2041 self.high = 1.
2042 self.loop = False
2043
2044 if trans == None:
2045 return Path([("M", self.x1, self.y1, not local), ("L", self.x2, self.y2, not local)], **self.attr)
2046 else:
2047 return Curve.Path(self, trans, local)
2048
2049 class LineGlobal:
2050 """Draws a line between two points, one or both of which is in
2051 global coordinates.
2052
2053 Line(x1, y1, x2, y2, lcoal1, local2, arrow_start, arrow_end, attribute=value)
2054
2055 x1, y1 required the starting point
2056 x2, y2 required the ending point
2057 local1 default=False if True, interpret first point as a
2058 local coordinate (apply transform)
2059 local2 default=False if True, interpret second point as a
2060 local coordinate (apply transform)
2061 arrow_start default=None if an identifier string/Unicode,
2062 draw a new arrow object at the
2063 beginning of the line; if a marker,
2064 draw that marker instead
2065 arrow_end default=None same for the end of the line
2066 attribute=value pairs keyword list SVG attributes
2067 """
2068 defaults = {}
2069
2070 def __repr__(self):
2071 local1, local2 = "", ""
2072 if self.local1: local1 = "L"
2073 if self.local2: local2 = "L"
2074
2075 return "<LineGlobal %s(%s, %s) to %s(%s, %s) %s>" % (local1, str(self.x1), str(self.y1), local2, str(self.x2), str(self.y2), self.attr)
2076
2077 def __init__(self, x1, y1, x2, y2, local1=False, local2=False, arrow_start=None, arrow_end=None, **attr):
2078 self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
2079 self.local1, self.local2 = local1, local2
2080 self.arrow_start, self.arrow_end = arrow_start, arrow_end
2081
2082 self.attr = dict(self.defaults)
2083 self.attr.update(attr)
2084
2085 def SVG(self, trans=None):
2086 """Apply the transformation "trans" and return an SVG object."""
2087 if isinstance(trans, str): trans = totrans(trans)
2088
2089 X1, Y1, X2, Y2 = self.x1, self.y1, self.x2, self.y2
2090
2091 if self.local1: X1, Y1 = trans(X1, Y1)
2092 if self.local2: X2, Y2 = trans(X2, Y2)
2093
2094 line = SVG("path", d="M%s %s L%s %s" % (X1, Y1, X2, Y2), **self.attr)
2095
2096 if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
2097 defs = SVG("defs")
2098
2099 if self.arrow_start != False and self.arrow_start != None:
2100 if isinstance(self.arrow_start, SVG):
2101 defs.append(self.arrow_start)
2102 line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
2103 elif isinstance(self.arrow_start, str):
2104 defs.append(make_marker(self.arrow_start, "arrow_start"))
2105 line.attr["marker-start"] = "url(#%s)" % self.arrow_start
2106 else:
2107 raise TypeError("arrow_start must be False/None or an id string for the new marker")
2108
2109 if self.arrow_end != False and self.arrow_end != None:
2110 if isinstance(self.arrow_end, SVG):
2111 defs.append(self.arrow_end)
2112 line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
2113 elif isinstance(self.arrow_end, str):
2114 defs.append(make_marker(self.arrow_end, "arrow_end"))
2115 line.attr["marker-end"] = "url(#%s)" % self.arrow_end
2116 else:
2117 raise TypeError("arrow_end must be False/None or an id string for the new marker")
2118
2119 return SVG("g", defs, line)
2120
2121 return line
2122
2123 class VLine(Line):
2124 """Draws a vertical line.
2125
2126 VLine(y1, y2, x, attribute=value)
2127
2128 y1, y2 required y range
2129 x required x position
2130 attribute=value pairs keyword list SVG attributes
2131 """
2132 defaults = {}
2133
2134 def __repr__(self):
2135 return "<VLine (%g, %g) at x=%s %s>" % (self.y1, self.y2, self.x, self.attr)
2136
2137 def __init__(self, y1, y2, x, **attr):
2138 self.x = x
2139 self.attr = dict(self.defaults)
2140 self.attr.update(attr)
2141 Line.__init__(self, x, y1, x, y2, **self.attr)
2142
2143 def Path(self, trans=None, local=False):
2144 """Apply the transformation "trans" and return a Path object in
2145 global coordinates. If local=True, return a Path in local coordinates
2146 (which must be transformed again)."""
2147 self.x1 = self.x
2148 self.x2 = self.x
2149 return Line.Path(self, trans, local)
2150
2151 class HLine(Line):
2152 """Draws a horizontal line.
2153
2154 HLine(x1, x2, y, attribute=value)
2155
2156 x1, x2 required x range
2157 y required y position
2158 attribute=value pairs keyword list SVG attributes
2159 """
2160 defaults = {}
2161
2162 def __repr__(self):
2163 return "<HLine (%g, %g) at y=%s %s>" % (self.x1, self.x2, self.y, self.attr)
2164
2165 def __init__(self, x1, x2, y, **attr):
2166 self.y = y
2167 self.attr = dict(self.defaults)
2168 self.attr.update(attr)
2169 Line.__init__(self, x1, y, x2, y, **self.attr)
2170
2171 def Path(self, trans=None, local=False):
2172 """Apply the transformation "trans" and return a Path object in
2173 global coordinates. If local=True, return a Path in local coordinates
2174 (which must be transformed again)."""
2175 self.y1 = self.y
2176 self.y2 = self.y
2177 return Line.Path(self, trans, local)
2178
2179
2180
2181 class Rect(Curve):
2182 """Draws a rectangle.
2183
2184 Rect(x1, y1, x2, y2, attribute=value)
2185
2186 x1, y1 required the starting point
2187 x2, y2 required the ending point
2188 attribute=value pairs keyword list SVG attributes
2189 """
2190 defaults = {}
2191
2192 def __repr__(self):
2193 return "<Rect (%g, %g), (%g, %g) %s>" % (self.x1, self.y1, self.x2, self.y2, self.attr)
2194
2195 def __init__(self, x1, y1, x2, y2, **attr):
2196 self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
2197
2198 self.attr = dict(self.defaults)
2199 self.attr.update(attr)
2200
2201 def SVG(self, trans=None):
2202 """Apply the transformation "trans" and return an SVG object."""
2203 return self.Path(trans).SVG()
2204
2205 def Path(self, trans=None, local=False):
2206 """Apply the transformation "trans" and return a Path object in
2207 global coordinates. If local=True, return a Path in local coordinates
2208 (which must be transformed again)."""
2209 if trans == None:
2210 return Path([("M", self.x1, self.y1, not local), ("L", self.x2, self.y1, not local), ("L", self.x2, self.y2, not local), ("L", self.x1, self.y2, not local), ("Z",)], **self.attr)
2211
2212 else:
2213 self.low = 0.
2214 self.high = 1.
2215 self.loop = False
2216
2217 self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1)
2218 d1 = Curve.Path(self, trans, local).d
2219
2220 self.f = lambda t: (self.x2, self.y1 + t*(self.y2 - self.y1))
2221 d2 = Curve.Path(self, trans, local).d
2222 del d2[0]
2223
2224 self.f = lambda t: (self.x2 + t*(self.x1 - self.x2), self.y2)
2225 d3 = Curve.Path(self, trans, local).d
2226 del d3[0]
2227
2228 self.f = lambda t: (self.x1, self.y2 + t*(self.y1 - self.y2))
2229 d4 = Curve.Path(self, trans, local).d
2230 del d4[0]
2231
2232 return Path(d=(d1 + d2 + d3 + d4 + [("Z",)]), **self.attr)
2233
2234
2235
2236 class Ellipse(Curve):
2237 """Draws an ellipse from a semimajor vector (ax,ay) and a semiminor
2238 length (b).
2239
2240 Ellipse(x, y, ax, ay, b, attribute=value)
2241
2242 x, y required the center of the ellipse/circle
2243 ax, ay required a vector indicating the length
2244 and direction of the semimajor axis
2245 b required the length of the semiminor axis.
2246 If equal to sqrt(ax2 + ay2), the
2247 ellipse is a circle
2248 attribute=value pairs keyword list SVG attributes
2249
2250 (If sqrt(ax**2 + ay**2) is less than b, then (ax,ay) is actually the
2251 semiminor axis.)
2252 """
2253 defaults = {}
2254
2255 def __repr__(self):
2256 return "<Ellipse (%g, %g) a=(%g, %g), b=%g %s>" % (self.x, self.y, self.ax, self.ay, self.b, self.attr)
2257
2258 def __init__(self, x, y, ax, ay, b, **attr):
2259 self.x, self.y, self.ax, self.ay, self.b = x, y, ax, ay, b
2260
2261 self.attr = dict(self.defaults)
2262 self.attr.update(attr)
2263
2264 def SVG(self, trans=None):
2265 """Apply the transformation "trans" and return an SVG object."""
2266 return self.Path(trans).SVG()
2267
2268 def Path(self, trans=None, local=False):
2269 """Apply the transformation "trans" and return a Path object in
2270 global coordinates. If local=True, return a Path in local coordinates
2271 (which must be transformed again)."""
2272 angle = math.atan2(self.ay, self.ax) + math.pi/2.
2273 bx = self.b * math.cos(angle)
2274 by = self.b * math.sin(angle)
2275
2276 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))
2277 self.low = -math.pi
2278 self.high = math.pi
2279 self.loop = True
2280 return Curve.Path(self, trans, local)
2281
2282
2283
2284 def unumber(x):
2285 """Converts numbers to a Unicode string, taking advantage of special
2286 Unicode characters to make nice minus signs and scientific notation.
2287 """
2288 output = u"%g" % x
2289
2290 if output[0] == u"-":
2291 output = u"\u2013" + output[1:]
2292
2293 index = output.find(u"e")
2294 if index != -1:
2295 uniout = unicode(output[:index]) + u"\u00d710"
2296 saw_nonzero = False
2297 for n in output[index+1:]:
2298 if n == u"+": pass
2299 elif n == u"-": uniout += u"\u207b"
2300 elif n == u"0":
2301 if saw_nonzero: uniout += u"\u2070"
2302 elif n == u"1":
2303 saw_nonzero = True
2304 uniout += u"\u00b9"
2305 elif n == u"2":
2306 saw_nonzero = True
2307 uniout += u"\u00b2"
2308 elif n == u"3":
2309 saw_nonzero = True
2310 uniout += u"\u00b3"
2311 elif u"4" <= n <= u"9":
2312 saw_nonzero = True
2313 if saw_nonzero: uniout += eval("u\"\\u%x\"" % (0x2070 + ord(n) - ord(u"0")))
2314 else: uniout += n
2315
2316 if uniout[:2] == u"1\u00d7": uniout = uniout[2:]
2317 return uniout
2318
2319 return output
2320
2321 class Ticks:
2322 """Superclass for all graphics primatives that draw ticks,
2323 miniticks, and tick labels. This class only draws the ticks.
2324
2325 Ticks(f, low, high, ticks, miniticks, labels, logbase, arrow_start,
2326 arrow_end, text_attr, attribute=value)
2327
2328 f required parametric function along which ticks
2329 will be drawn; has the same format as
2330 the function used in Curve
2331 low, high required range of the independent variable
2332 ticks default=-10 request ticks according to the standard
2333 tick specification (see below)
2334 miniticks default=True request miniticks according to the
2335 standard minitick specification (below)
2336 labels True request tick labels according to the
2337 standard tick label specification (below)
2338 logbase default=None if a number, the axis is logarithmic with
2339 ticks at the given base (usually 10)
2340 arrow_start default=None if a new string identifier, draw an arrow
2341 at the low-end of the axis, referenced by
2342 that identifier; if an SVG marker object,
2343 use that marker
2344 arrow_end default=None if a new string identifier, draw an arrow
2345 at the high-end of the axis, referenced by
2346 that identifier; if an SVG marker object,
2347 use that marker
2348 text_attr default={} SVG attributes for the text labels
2349 attribute=value pairs keyword list SVG attributes for the tick marks
2350
2351 Standard tick specification:
2352
2353 * True: same as -10 (below).
2354 * Positive number N: draw exactly N ticks, including the endpoints. To
2355 subdivide an axis into 10 equal-sized segments, ask for 11 ticks.
2356 * Negative number -N: draw at least N ticks. Ticks will be chosen with
2357 "natural" values, multiples of 2 or 5.
2358 * List of values: draw a tick mark at each value.
2359 * Dict of value, label pairs: draw a tick mark at each value, labeling
2360 it with the given string. This lets you say things like {3.14159: "pi"}.
2361 * False or None: no ticks.
2362
2363 Standard minitick specification:
2364
2365 * True: draw miniticks with "natural" values, more closely spaced than
2366 the ticks.
2367 * Positive number N: draw exactly N miniticks, including the endpoints.
2368 To subdivide an axis into 100 equal-sized segments, ask for 101 miniticks.
2369 * Negative number -N: draw at least N miniticks.
2370 * List of values: draw a minitick mark at each value.
2371 * False or None: no miniticks.
2372
2373 Standard tick label specification:
2374
2375 * True: use the unumber function (described below)
2376 * Format string: standard format strings, e.g. "%5.2f" for 12.34
2377 * Python callable: function that converts numbers to strings
2378 * False or None: no labels
2379 """
2380 defaults = {"stroke-width":"0.25pt"}
2381 text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
2382 tick_start = -1.5
2383 tick_end = 1.5
2384 minitick_start = -0.75
2385 minitick_end = 0.75
2386 text_start = 2.5
2387 text_angle = 0.
2388
2389 def __repr__(self):
2390 return "<Ticks %s from %s to %s ticks=%s labels=%s %s>" % (self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr)
2391
2392 def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, text_attr={}, **attr):
2393 self.f = f
2394 self.low = low
2395 self.high = high
2396 self.ticks = ticks
2397 self.miniticks = miniticks
2398 self.labels = labels
2399 self.logbase = logbase
2400 self.arrow_start = arrow_start
2401 self.arrow_end = arrow_end
2402
2403 self.attr = dict(self.defaults)
2404 self.attr.update(attr)
2405
2406 self.text_attr = dict(self.text_defaults)
2407 self.text_attr.update(text_attr)
2408
2409 def orient_tickmark(self, t, trans=None):
2410 """Return the position, normalized local x vector, normalized
2411 local y vector, and angle of a tick at position t.
2412
2413 Normally only used internally.
2414 """
2415 if isinstance(trans, str): trans = totrans(trans)
2416 if trans == None:
2417 f = self.f
2418 else:
2419 f = lambda t: trans(*self.f(t))
2420
2421 eps = _epsilon * abs(self.high - self.low)
2422
2423 X, Y = f(t)
2424 Xprime, Yprime = f(t + eps)
2425 xhatx, xhaty = (Xprime - X)/eps, (Yprime - Y)/eps
2426
2427 norm = math.sqrt(xhatx**2 + xhaty**2)
2428 if norm != 0: xhatx, xhaty = xhatx/norm, xhaty/norm
2429 else: xhatx, xhaty = 1., 0.
2430
2431 angle = math.atan2(xhaty, xhatx) + math.pi/2.
2432 yhatx, yhaty = math.cos(angle), math.sin(angle)
2433
2434 return (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle
2435
2436 def SVG(self, trans=None):
2437 """Apply the transformation "trans" and return an SVG object."""
2438 if isinstance(trans, str): trans = totrans(trans)
2439
2440 self.last_ticks, self.last_miniticks = self.interpret()
2441 tickmarks = Path([], **self.attr)
2442 minitickmarks = Path([], **self.attr)
2443 output = SVG("g")
2444
2445 if (self.arrow_start != False and self.arrow_start != None) or (self.arrow_end != False and self.arrow_end != None):
2446 defs = SVG("defs")
2447
2448 if self.arrow_start != False and self.arrow_start != None:
2449 if isinstance(self.arrow_start, SVG):
2450 defs.append(self.arrow_start)
2451 elif isinstance(self.arrow_start, str):
2452 defs.append(make_marker(self.arrow_start, "arrow_start"))
2453 else:
2454 raise TypeError("arrow_start must be False/None or an id string for the new marker")
2455
2456 if self.arrow_end != False and self.arrow_end != None:
2457 if isinstance(self.arrow_end, SVG):
2458 defs.append(self.arrow_end)
2459 elif isinstance(self.arrow_end, str):
2460 defs.append(make_marker(self.arrow_end, "arrow_end"))
2461 else:
2462 raise TypeError("arrow_end must be False/None or an id string for the new marker")
2463
2464 output.append(defs)
2465
2466 eps = _epsilon * (self.high - self.low)
2467
2468 for t, label in self.last_ticks.items():
2469 (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
2470
2471 if (not self.arrow_start or abs(t - self.low) > eps) and (not self.arrow_end or abs(t - self.high) > eps):
2472 tickmarks.d.append(("M", X - yhatx*self.tick_start, Y - yhaty*self.tick_start, True))
2473 tickmarks.d.append(("L", X - yhatx*self.tick_end, Y - yhaty*self.tick_end, True))
2474
2475 angle = (angle - math.pi/2.)*180./math.pi + self.text_angle
2476
2477
2478 if _hacks["inkscape-text-vertical-shift"]:
2479 if self.text_start > 0:
2480 X += math.cos(angle*math.pi/180. + math.pi/2.) * 2.
2481 Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2.
2482 else:
2483 X += math.cos(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
2484 Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
2485
2486
2487 if label != "":
2488 output.append(SVG("text", label, transform="translate(%g, %g) rotate(%g)" % \
2489 (X - yhatx*self.text_start, Y - yhaty*self.text_start, angle), **self.text_attr))
2490
2491 for t in self.last_miniticks:
2492 skip = False
2493 for tt in self.last_ticks.keys():
2494 if abs(t - tt) < eps:
2495 skip = True
2496 break
2497 if not skip:
2498 (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
2499
2500 if (not self.arrow_start or abs(t - self.low) > eps) and (not self.arrow_end or abs(t - self.high) > eps):
2501 minitickmarks.d.append(("M", X - yhatx*self.minitick_start, Y - yhaty*self.minitick_start, True))
2502 minitickmarks.d.append(("L", X - yhatx*self.minitick_end, Y - yhaty*self.minitick_end, True))
2503
2504 output.prepend(tickmarks.SVG(trans))
2505 output.prepend(minitickmarks.SVG(trans))
2506 return output
2507
2508 def interpret(self):
2509 """Evaluate and return optimal ticks and miniticks according to
2510 the standard minitick specification.
2511
2512 Normally only used internally.
2513 """
2514
2515 if self.labels == None or self.labels == False:
2516 format = lambda x: ""
2517
2518 elif self.labels == True:
2519 format = unumber
2520
2521 elif isinstance(self.labels, str):
2522 format = lambda x: (self.labels % x)
2523
2524 elif callable(self.labels):
2525 format = self.labels
2526
2527 else: raise TypeError("labels must be None/False, True, a format string, or a number->string function")
2528
2529
2530 ticks = self.ticks
2531
2532
2533 if ticks == None or ticks == False: return {}, []
2534
2535
2536 elif isinstance(ticks, (int, long)):
2537 if ticks == True: ticks = -10
2538
2539 if self.logbase == None:
2540 ticks = self.compute_ticks(ticks, format)
2541 else:
2542 ticks = self.compute_logticks(self.logbase, ticks, format)
2543
2544
2545 if self.miniticks == True:
2546 if self.logbase == None:
2547 return ticks, self.compute_miniticks(ticks)
2548 else:
2549 return ticks, self.compute_logminiticks(self.logbase)
2550
2551 elif isinstance(self.miniticks, (int, long)):
2552 return ticks, self.regular_miniticks(self.miniticks)
2553
2554 elif getattr(self.miniticks, "__iter__", False):
2555 return ticks, self.miniticks
2556
2557 elif self.miniticks == False or self.miniticks == None:
2558 return ticks, []
2559
2560 else:
2561 raise TypeError("miniticks must be None/False, True, a number of desired miniticks, or a list of numbers")
2562
2563
2564 elif getattr(ticks, "__iter__", False):
2565
2566
2567 if not isinstance(ticks, dict):
2568 output = {}
2569 eps = _epsilon * (self.high - self.low)
2570 for x in ticks:
2571 if format == unumber and abs(x) < eps:
2572 output[x] = u"0"
2573 else:
2574 output[x] = format(x)
2575 ticks = output
2576
2577
2578 else: pass
2579
2580
2581 if self.miniticks == True:
2582 if self.logbase == None:
2583 return ticks, self.compute_miniticks(ticks)
2584 else:
2585 return ticks, self.compute_logminiticks(self.logbase)
2586
2587 elif isinstance(self.miniticks, (int, long)):
2588 return ticks, self.regular_miniticks(self.miniticks)
2589
2590 elif getattr(self.miniticks, "__iter__", False):
2591 return ticks, self.miniticks
2592
2593 elif self.miniticks == False or self.miniticks == None:
2594 return ticks, []
2595
2596 else:
2597 raise TypeError("miniticks must be None/False, True, a number of desired miniticks, or a list of numbers")
2598
2599 else:
2600 raise TypeError("ticks must be None/False, a number of desired ticks, a list of numbers, or a dictionary of explicit markers")
2601
2602 def compute_ticks(self, N, format):
2603 """Return less than -N or exactly N optimal linear ticks.
2604
2605 Normally only used internally.
2606 """
2607 if self.low >= self.high: raise ValueError("low must be less than high")
2608 if N == 1: raise ValueError("N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum")
2609
2610 eps = _epsilon * (self.high - self.low)
2611
2612 if N >= 0:
2613 output = {}
2614 x = self.low
2615 for i in range(N):
2616 if format == unumber and abs(x) < eps: label = u"0"
2617 else: label = format(x)
2618 output[x] = label
2619 x += (self.high - self.low)/(N-1.)
2620 return output
2621
2622 N = -N
2623
2624 counter = 0
2625 granularity = 10**math.ceil(math.log10(max(abs(self.low), abs(self.high))))
2626 lowN = math.ceil(1.*self.low / granularity)
2627 highN = math.floor(1.*self.high / granularity)
2628
2629 while (lowN > highN):
2630 countermod3 = counter % 3
2631 if countermod3 == 0: granularity *= 0.5
2632 elif countermod3 == 1: granularity *= 0.4
2633 else: granularity *= 0.5
2634 counter += 1
2635 lowN = math.ceil(1.*self.low / granularity)
2636 highN = math.floor(1.*self.high / granularity)
2637
2638 last_granularity = granularity
2639 last_trial = None
2640
2641 while True:
2642 trial = {}
2643 for n in range(int(lowN), int(highN)+1):
2644 x = n * granularity
2645 if format == unumber and abs(x) < eps: label = u"0"
2646 else: label = format(x)
2647 trial[x] = label
2648
2649 if int(highN)+1 - int(lowN) >= N:
2650 if last_trial == None:
2651 v1, v2 = self.low, self.high
2652 return {v1: format(v1), v2: format(v2)}
2653 else:
2654 low_in_ticks, high_in_ticks = False, False
2655 for t in last_trial.keys():
2656 if 1.*abs(t - self.low)/last_granularity < _epsilon: low_in_ticks = True
2657 if 1.*abs(t - self.high)/last_granularity < _epsilon: high_in_ticks = True
2658
2659 lowN = 1.*self.low / last_granularity
2660 highN = 1.*self.high / last_granularity
2661 if abs(lowN - round(lowN)) < _epsilon and not low_in_ticks:
2662 last_trial[self.low] = format(self.low)
2663 if abs(highN - round(highN)) < _epsilon and not high_in_ticks:
2664 last_trial[self.high] = format(self.high)
2665 return last_trial
2666
2667 last_granularity = granularity
2668 last_trial = trial
2669
2670 countermod3 = counter % 3
2671 if countermod3 == 0: granularity *= 0.5
2672 elif countermod3 == 1: granularity *= 0.4
2673 else: granularity *= 0.5
2674 counter += 1
2675 lowN = math.ceil(1.*self.low / granularity)
2676 highN = math.floor(1.*self.high / granularity)
2677
2678 def regular_miniticks(self, N):
2679 """Return exactly N linear ticks.
2680
2681 Normally only used internally.
2682 """
2683 output = []
2684 x = self.low
2685 for i in range(N):
2686 output.append(x)
2687 x += (self.high - self.low)/(N-1.)
2688 return output
2689
2690 def compute_miniticks(self, original_ticks):
2691 """Return optimal linear miniticks, given a set of ticks.
2692
2693 Normally only used internally.
2694 """
2695 if len(original_ticks) < 2: original_ticks = ticks(self.low, self.high)
2696 original_ticks = sorted(original_ticks.keys())
2697
2698 if self.low > original_ticks[0] + _epsilon or self.high < original_ticks[-1] - _epsilon:
2699 raise ValueError("original_ticks {%g...%g} extend beyond [%g, %g]" % (original_ticks[0], original_ticks[-1], self.low, self.high))
2700
2701 granularities = []
2702 for i in range(len(original_ticks)-1):
2703 granularities.append(original_ticks[i+1] - original_ticks[i])
2704 spacing = 10**(math.ceil(math.log10(min(granularities)) - 1))
2705
2706 output = []
2707 x = original_ticks[0] - math.ceil(1.*(original_ticks[0] - self.low) / spacing) * spacing
2708
2709 while x <= self.high:
2710 if x >= self.low:
2711 already_in_ticks = False
2712 for t in original_ticks:
2713 if abs(x-t) < _epsilon * (self.high - self.low): already_in_ticks = True
2714 if not already_in_ticks: output.append(x)
2715 x += spacing
2716 return output
2717
2718 def compute_logticks(self, base, N, format):
2719 """Return less than -N or exactly N optimal logarithmic ticks.
2720
2721 Normally only used internally.
2722 """
2723 if self.low >= self.high: raise ValueError("low must be less than high")
2724 if N == 1: raise ValueError("N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum")
2725
2726 eps = _epsilon * (self.high - self.low)
2727
2728 if N >= 0:
2729 output = {}
2730 x = self.low
2731 for i in range(N):
2732 if format == unumber and abs(x) < eps: label = u"0"
2733 else: label = format(x)
2734 output[x] = label
2735 x += (self.high - self.low)/(N-1.)
2736 return output
2737
2738 N = -N
2739
2740 lowN = math.floor(math.log(self.low, base))
2741 highN = math.ceil(math.log(self.high, base))
2742 output = {}
2743 for n in range(int(lowN), int(highN)+1):
2744 x = base**n
2745 label = format(x)
2746 if self.low <= x <= self.high: output[x] = label
2747
2748 for i in range(1, len(output)):
2749 keys = sorted(output.keys())
2750 keys = keys[::i]
2751 values = map(lambda k: output[k], keys)
2752 if len(values) <= N:
2753 for k in output.keys():
2754 if k not in keys:
2755 output[k] = ""
2756 break
2757
2758 if len(output) <= 2:
2759 output2 = self.compute_ticks(N=-int(math.ceil(N/2.)), format=format)
2760 lowest = min(output2)
2761
2762 for k in output:
2763 if k < lowest: output2[k] = output[k]
2764 output = output2
2765
2766 return output
2767
2768 def compute_logminiticks(self, base):
2769 """Return optimal logarithmic miniticks, given a set of ticks.
2770
2771 Normally only used internally.
2772 """
2773 if self.low >= self.high: raise ValueError("low must be less than high")
2774
2775 lowN = math.floor(math.log(self.low, base))
2776 highN = math.ceil(math.log(self.high, base))
2777 output = []
2778 num_ticks = 0
2779 for n in range(int(lowN), int(highN)+1):
2780 x = base**n
2781 if self.low <= x <= self.high: num_ticks += 1
2782 for m in range(2, int(math.ceil(base))):
2783 minix = m * x
2784 if self.low <= minix <= self.high: output.append(minix)
2785
2786 if num_ticks <= 2: return []
2787 else: return output
2788
2789
2790
2791 class CurveAxis(Curve, Ticks):
2792 """Draw an axis with tick marks along a parametric curve.
2793
2794 CurveAxis(f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
2795 text_attr, attribute=value)
2796
2797 f required a Python callable or string in
2798 the form "f(t), g(t)", just like Curve
2799 low, high required left and right endpoints
2800 ticks default=-10 request ticks according to the standard
2801 tick specification (see help(Ticks))
2802 miniticks default=True request miniticks according to the
2803 standard minitick specification
2804 labels True request tick labels according to the
2805 standard tick label specification
2806 logbase default=None if a number, the x axis is logarithmic
2807 with ticks at the given base (10 being
2808 the most common)
2809 arrow_start default=None if a new string identifier, draw an
2810 arrow at the low-end of the axis,
2811 referenced by that identifier; if an
2812 SVG marker object, use that marker
2813 arrow_end default=None if a new string identifier, draw an
2814 arrow at the high-end of the axis,
2815 referenced by that identifier; if an
2816 SVG marker object, use that marker
2817 text_attr default={} SVG attributes for the text labels
2818 attribute=value pairs keyword list SVG attributes
2819 """
2820 defaults = {"stroke-width":"0.25pt"}
2821 text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
2822
2823 def __repr__(self):
2824 return "<CurveAxis %s [%s, %s] ticks=%s labels=%s %s>" % (self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr)
2825
2826 def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, text_attr={}, **attr):
2827 tattr = dict(self.text_defaults)
2828 tattr.update(text_attr)
2829 Curve.__init__(self, f, low, high)
2830 Ticks.__init__(self, f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
2831
2832 def SVG(self, trans=None):
2833 """Apply the transformation "trans" and return an SVG object."""
2834 func = Curve.SVG(self, trans)
2835 ticks = Ticks.SVG(self, trans)
2836
2837 if self.arrow_start != False and self.arrow_start != None:
2838 if isinstance(self.arrow_start, str):
2839 func.attr["marker-start"] = "url(#%s)" % self.arrow_start
2840 else:
2841 func.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
2842
2843 if self.arrow_end != False and self.arrow_end != None:
2844 if isinstance(self.arrow_end, str):
2845 func.attr["marker-end"] = "url(#%s)" % self.arrow_end
2846 else:
2847 func.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
2848
2849 ticks.append(func)
2850 return ticks
2851
2852 class LineAxis(Line, Ticks):
2853 """Draws an axis with tick marks along a line.
2854
2855 LineAxis(x1, y1, x2, y2, start, end, ticks, miniticks, labels, logbase,
2856 arrow_start, arrow_end, text_attr, attribute=value)
2857
2858 x1, y1 required starting point
2859 x2, y2 required ending point
2860 start, end default=0, 1 values to start and end labeling
2861 ticks default=-10 request ticks according to the standard
2862 tick specification (see help(Ticks))
2863 miniticks default=True request miniticks according to the
2864 standard minitick specification
2865 labels True request tick labels according to the
2866 standard tick label specification
2867 logbase default=None if a number, the x axis is logarithmic
2868 with ticks at the given base (usually 10)
2869 arrow_start default=None if a new string identifier, draw an arrow
2870 at the low-end of the axis, referenced by
2871 that identifier; if an SVG marker object,
2872 use that marker
2873 arrow_end default=None if a new string identifier, draw an arrow
2874 at the high-end of the axis, referenced by
2875 that identifier; if an SVG marker object,
2876 use that marker
2877 text_attr default={} SVG attributes for the text labels
2878 attribute=value pairs keyword list SVG attributes
2879 """
2880 defaults = {"stroke-width":"0.25pt"}
2881 text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
2882
2883 def __repr__(self):
2884 return "<LineAxis (%g, %g) to (%g, %g) ticks=%s labels=%s %s>" % (self.x1, self.y1, self.x2, self.y2, str(self.ticks), str(self.labels), self.attr)
2885
2886 def __init__(self, x1, y1, x2, y2, start=0., end=1., ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
2887 self.start = start
2888 self.end = end
2889 self.exclude = exclude
2890 tattr = dict(self.text_defaults)
2891 tattr.update(text_attr)
2892 Line.__init__(self, x1, y1, x2, y2, **attr)
2893 Ticks.__init__(self, None, None, None, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
2894
2895 def interpret(self):
2896 if self.exclude != None and not (isinstance(self.exclude, (tuple, list)) and len(self.exclude) == 2 and \
2897 isinstance(self.exclude[0], (int, long, float)) and isinstance(self.exclude[1], (int, long, float))):
2898 raise TypeError("exclude must either be None or (low, high)")
2899
2900 ticks, miniticks = Ticks.interpret(self)
2901 if self.exclude == None: return ticks, miniticks
2902
2903 ticks2 = {}
2904 for loc, label in ticks.items():
2905 if self.exclude[0] <= loc <= self.exclude[1]:
2906 ticks2[loc] = ""
2907 else:
2908 ticks2[loc] = label
2909
2910 return ticks2, miniticks
2911
2912 def SVG(self, trans=None):
2913 """Apply the transformation "trans" and return an SVG object."""
2914 line = Line.SVG(self, trans)
2915
2916 f01 = self.f
2917 self.f = lambda t: f01(1. * (t - self.start) / (self.end - self.start))
2918 self.low = self.start
2919 self.high = self.end
2920
2921 if self.arrow_start != False and self.arrow_start != None:
2922 if isinstance(self.arrow_start, str):
2923 line.attr["marker-start"] = "url(#%s)" % self.arrow_start
2924 else:
2925 line.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
2926
2927 if self.arrow_end != False and self.arrow_end != None:
2928 if isinstance(self.arrow_end, str):
2929 line.attr["marker-end"] = "url(#%s)" % self.arrow_end
2930 else:
2931 line.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
2932
2933 ticks = Ticks.SVG(self, trans)
2934 ticks.append(line)
2935 return ticks
2936
2937 class XAxis(LineAxis):
2938 """Draws an x axis with tick marks.
2939
2940 XAxis(xmin, xmax, aty, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
2941 exclude, text_attr, attribute=value)
2942
2943 xmin, xmax required the x range
2944 aty default=0 y position to draw the axis
2945 ticks default=-10 request ticks according to the standard
2946 tick specification (see help(Ticks))
2947 miniticks default=True request miniticks according to the
2948 standard minitick specification
2949 labels True request tick labels according to the
2950 standard tick label specification
2951 logbase default=None if a number, the x axis is logarithmic
2952 with ticks at the given base (usually 10)
2953 arrow_start default=None if a new string identifier, draw an arrow
2954 at the low-end of the axis, referenced by
2955 that identifier; if an SVG marker object,
2956 use that marker
2957 arrow_end default=None if a new string identifier, draw an arrow
2958 at the high-end of the axis, referenced by
2959 that identifier; if an SVG marker object,
2960 use that marker
2961 exclude default=None if a (low, high) pair, don't draw text
2962 labels within this range
2963 text_attr default={} SVG attributes for the text labels
2964 attribute=value pairs keyword list SVG attributes for all lines
2965
2966 The exclude option is provided for Axes to keep text from overlapping
2967 where the axes cross. Normal users are not likely to need it.
2968 """
2969 defaults = {"stroke-width":"0.25pt"}
2970 text_defaults = {"stroke":"none", "fill":"black", "font-size":5, "dominant-baseline":"text-before-edge"}
2971 text_start = -1.
2972 text_angle = 0.
2973
2974 def __repr__(self):
2975 return "<XAxis (%g, %g) at y=%g ticks=%s labels=%s %s>" % (self.xmin, self.xmax, self.aty, str(self.ticks), str(self.labels), self.attr)
2976
2977 def __init__(self, xmin, xmax, aty=0, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
2978 self.aty = aty
2979 tattr = dict(self.text_defaults)
2980 tattr.update(text_attr)
2981 LineAxis.__init__(self, xmin, aty, xmax, aty, xmin, xmax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
2982
2983 def SVG(self, trans=None):
2984 """Apply the transformation "trans" and return an SVG object."""
2985 self.y1 = self.aty
2986 self.y2 = self.aty
2987 return LineAxis.SVG(self, trans)
2988
2989 class YAxis(LineAxis):
2990 """Draws a y axis with tick marks.
2991
2992 YAxis(ymin, ymax, atx, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
2993 exclude, text_attr, attribute=value)
2994
2995 ymin, ymax required the y range
2996 atx default=0 x position to draw the axis
2997 ticks default=-10 request ticks according to the standard
2998 tick specification (see help(Ticks))
2999 miniticks default=True request miniticks according to the
3000 standard minitick specification
3001 labels True request tick labels according to the
3002 standard tick label specification
3003 logbase default=None if a number, the y axis is logarithmic
3004 with ticks at the given base (usually 10)
3005 arrow_start default=None if a new string identifier, draw an arrow
3006 at the low-end of the axis, referenced by
3007 that identifier; if an SVG marker object,
3008 use that marker
3009 arrow_end default=None if a new string identifier, draw an arrow
3010 at the high-end of the axis, referenced by
3011 that identifier; if an SVG marker object,
3012 use that marker
3013 exclude default=None if a (low, high) pair, don't draw text
3014 labels within this range
3015 text_attr default={} SVG attributes for the text labels
3016 attribute=value pairs keyword list SVG attributes for all lines
3017
3018 The exclude option is provided for Axes to keep text from overlapping
3019 where the axes cross. Normal users are not likely to need it.
3020 """
3021 defaults = {"stroke-width":"0.25pt"}
3022 text_defaults = {"stroke":"none", "fill":"black", "font-size":5, "text-anchor":"end", "dominant-baseline":"middle"}
3023 text_start = 2.5
3024 text_angle = 90.
3025
3026 def __repr__(self):
3027 return "<YAxis (%g, %g) at x=%g ticks=%s labels=%s %s>" % (self.ymin, self.ymax, self.atx, str(self.ticks), str(self.labels), self.attr)
3028
3029 def __init__(self, ymin, ymax, atx=0, ticks=-10, miniticks=True, labels=True, logbase=None, arrow_start=None, arrow_end=None, exclude=None, text_attr={}, **attr):
3030 self.atx = atx
3031 tattr = dict(self.text_defaults)
3032 tattr.update(text_attr)
3033 LineAxis.__init__(self, atx, ymin, atx, ymax, ymin, ymax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
3034
3035 def SVG(self, trans=None):
3036 """Apply the transformation "trans" and return an SVG object."""
3037 self.x1 = self.atx
3038 self.x2 = self.atx
3039 return LineAxis.SVG(self, trans)
3040
3041 class Axes:
3042 """Draw a pair of intersecting x-y axes.
3043
3044 Axes(xmin, xmax, ymin, ymax, atx, aty, xticks, xminiticks, xlabels, xlogbase,
3045 yticks, yminiticks, ylabels, ylogbase, arrows, text_attr, attribute=value)
3046
3047 xmin, xmax required the x range
3048 ymin, ymax required the y range
3049 atx, aty default=0, 0 point where the axes try to cross;
3050 if outside the range, the axes will
3051 cross at the closest corner
3052 xticks default=-10 request ticks according to the standard
3053 tick specification (see help(Ticks))
3054 xminiticks default=True request miniticks according to the
3055 standard minitick specification
3056 xlabels True request tick labels according to the
3057 standard tick label specification
3058 xlogbase default=None if a number, the x axis is logarithmic
3059 with ticks at the given base (usually 10)
3060 yticks default=-10 request ticks according to the standard
3061 tick specification
3062 yminiticks default=True request miniticks according to the
3063 standard minitick specification
3064 ylabels True request tick labels according to the
3065 standard tick label specification
3066 ylogbase default=None if a number, the y axis is logarithmic
3067 with ticks at the given base (usually 10)
3068 arrows default=None if a new string identifier, draw arrows
3069 referenced by that identifier
3070 text_attr default={} SVG attributes for the text labels
3071 attribute=value pairs keyword list SVG attributes for all lines
3072 """
3073 defaults = {"stroke-width":"0.25pt"}
3074 text_defaults = {"stroke":"none", "fill":"black", "font-size":5}
3075
3076 def __repr__(self):
3077 return "<Axes x=(%g, %g) y=(%g, %g) at (%g, %g) %s>" % (self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty, self.attr)
3078
3079 def __init__(self, xmin, xmax, ymin, ymax, atx=0, aty=0, xticks=-10, xminiticks=True, xlabels=True, xlogbase=None, yticks=-10, yminiticks=True, ylabels=True, ylogbase=None, arrows=None, text_attr={}, **attr):
3080 self.xmin, self.xmax = xmin, xmax
3081 self.ymin, self.ymax = ymin, ymax
3082 self.atx, self.aty = atx, aty
3083 self.xticks, self.xminiticks, self.xlabels, self.xlogbase = xticks, xminiticks, xlabels, xlogbase
3084 self.yticks, self.yminiticks, self.ylabels, self.ylogbase = yticks, yminiticks, ylabels, ylogbase
3085 self.arrows = arrows
3086
3087 self.text_attr = dict(self.text_defaults)
3088 self.text_attr.update(text_attr)
3089
3090 self.attr = dict(self.defaults)
3091 self.attr.update(attr)
3092
3093 def SVG(self, trans=None):
3094 """Apply the transformation "trans" and return an SVG object."""
3095 atx, aty = self.atx, self.aty
3096 if atx < self.xmin: atx = self.xmin
3097 if atx > self.xmax: atx = self.xmax
3098 if aty < self.ymin: aty = self.ymin
3099 if aty > self.ymax: aty = self.ymax
3100
3101 xmargin = 0.1 * abs(self.ymin - self.ymax)
3102 xexclude = atx - xmargin, atx + xmargin
3103
3104 ymargin = 0.1 * abs(self.xmin - self.xmax)
3105 yexclude = aty - ymargin, aty + ymargin
3106
3107 if self.arrows != None and self.arrows != False:
3108 xarrow_start = self.arrows + ".xstart"
3109 xarrow_end = self.arrows + ".xend"
3110 yarrow_start = self.arrows + ".ystart"
3111 yarrow_end = self.arrows + ".yend"
3112 else:
3113 xarrow_start = xarrow_end = yarrow_start = yarrow_end = None
3114
3115 xaxis = XAxis(self.xmin, self.xmax, aty, self.xticks, self.xminiticks, self.xlabels, self.xlogbase, xarrow_start, xarrow_end, exclude=xexclude, text_attr=self.text_attr, **self.attr).SVG(trans)
3116 yaxis = YAxis(self.ymin, self.ymax, atx, self.yticks, self.yminiticks, self.ylabels, self.ylogbase, yarrow_start, yarrow_end, exclude=yexclude, text_attr=self.text_attr, **self.attr).SVG(trans)
3117 return SVG("g", *(xaxis.sub + yaxis.sub))
3118
3119
3120
3121 class HGrid(Ticks):
3122 """Draws the horizontal lines of a grid over a specified region
3123 using the standard tick specification (see help(Ticks)) to place the
3124 grid lines.
3125
3126 HGrid(xmin, xmax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
3127
3128 xmin, xmax required the x range
3129 low, high required the y range
3130 ticks default=-10 request ticks according to the standard
3131 tick specification (see help(Ticks))
3132 miniticks default=False request miniticks according to the
3133 standard minitick specification
3134 logbase default=None if a number, the axis is logarithmic
3135 with ticks at the given base (usually 10)
3136 mini_attr default={} SVG attributes for the minitick-lines
3137 (if miniticks != False)
3138 attribute=value pairs keyword list SVG attributes for the major tick lines
3139 """
3140 defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
3141 mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
3142
3143 def __repr__(self):
3144 return "<HGrid x=(%g, %g) %g <= y <= %g ticks=%s miniticks=%s %s>" % (self.xmin, self.xmax, self.low, self.high, str(self.ticks), str(self.miniticks), self.attr)
3145
3146 def __init__(self, xmin, xmax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
3147 self.xmin, self.xmax = xmin, xmax
3148
3149 self.mini_attr = dict(self.mini_defaults)
3150 self.mini_attr.update(mini_attr)
3151
3152 Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
3153
3154 self.attr = dict(self.defaults)
3155 self.attr.update(attr)
3156
3157 def SVG(self, trans=None):
3158 """Apply the transformation "trans" and return an SVG object."""
3159 self.last_ticks, self.last_miniticks = Ticks.interpret(self)
3160
3161 ticksd = []
3162 for t in self.last_ticks.keys():
3163 ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3164
3165 miniticksd = []
3166 for t in self.last_miniticks:
3167 miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3168
3169 return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
3170
3171 class VGrid(Ticks):
3172 """Draws the vertical lines of a grid over a specified region
3173 using the standard tick specification (see help(Ticks)) to place the
3174 grid lines.
3175
3176 HGrid(ymin, ymax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
3177
3178 ymin, ymax required the y range
3179 low, high required the x range
3180 ticks default=-10 request ticks according to the standard
3181 tick specification (see help(Ticks))
3182 miniticks default=False request miniticks according to the
3183 standard minitick specification
3184 logbase default=None if a number, the axis is logarithmic
3185 with ticks at the given base (usually 10)
3186 mini_attr default={} SVG attributes for the minitick-lines
3187 (if miniticks != False)
3188 attribute=value pairs keyword list SVG attributes for the major tick lines
3189 """
3190 defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
3191 mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
3192
3193 def __repr__(self):
3194 return "<VGrid y=(%g, %g) %g <= x <= %g ticks=%s miniticks=%s %s>" % (self.ymin, self.ymax, self.low, self.high, str(self.ticks), str(self.miniticks), self.attr)
3195
3196 def __init__(self, ymin, ymax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
3197 self.ymin, self.ymax = ymin, ymax
3198
3199 self.mini_attr = dict(self.mini_defaults)
3200 self.mini_attr.update(mini_attr)
3201
3202 Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
3203
3204 self.attr = dict(self.defaults)
3205 self.attr.update(attr)
3206
3207 def SVG(self, trans=None):
3208 """Apply the transformation "trans" and return an SVG object."""
3209 self.last_ticks, self.last_miniticks = Ticks.interpret(self)
3210
3211 ticksd = []
3212 for t in self.last_ticks.keys():
3213 ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3214
3215 miniticksd = []
3216 for t in self.last_miniticks:
3217 miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3218
3219 return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
3220
3221 class Grid(Ticks):
3222 """Draws a grid over a specified region using the standard tick
3223 specification (see help(Ticks)) to place the grid lines.
3224
3225 Grid(xmin, xmax, ymin, ymax, ticks, miniticks, logbase, mini_attr, attribute=value)
3226
3227 xmin, xmax required the x range
3228 ymin, ymax required the y range
3229 ticks default=-10 request ticks according to the standard
3230 tick specification (see help(Ticks))
3231 miniticks default=False request miniticks according to the
3232 standard minitick specification
3233 logbase default=None if a number, the axis is logarithmic
3234 with ticks at the given base (usually 10)
3235 mini_attr default={} SVG attributes for the minitick-lines
3236 (if miniticks != False)
3237 attribute=value pairs keyword list SVG attributes for the major tick lines
3238 """
3239 defaults = {"stroke-width":"0.25pt", "stroke":"gray"}
3240 mini_defaults = {"stroke-width":"0.25pt", "stroke":"lightgray", "stroke-dasharray":"1,1"}
3241
3242 def __repr__(self):
3243 return "<Grid x=(%g, %g) y=(%g, %g) ticks=%s miniticks=%s %s>" % (self.xmin, self.xmax, self.ymin, self.ymax, str(self.ticks), str(self.miniticks), self.attr)
3244
3245 def __init__(self, xmin, xmax, ymin, ymax, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
3246 self.xmin, self.xmax = xmin, xmax
3247 self.ymin, self.ymax = ymin, ymax
3248
3249 self.mini_attr = dict(self.mini_defaults)
3250 self.mini_attr.update(mini_attr)
3251
3252 Ticks.__init__(self, None, None, None, ticks, miniticks, None, logbase)
3253
3254 self.attr = dict(self.defaults)
3255 self.attr.update(attr)
3256
3257 def SVG(self, trans=None):
3258 """Apply the transformation "trans" and return an SVG object."""
3259 self.low, self.high = self.xmin, self.xmax
3260 self.last_xticks, self.last_xminiticks = Ticks.interpret(self)
3261 self.low, self.high = self.ymin, self.ymax
3262 self.last_yticks, self.last_yminiticks = Ticks.interpret(self)
3263
3264 ticksd = []
3265 for t in self.last_xticks.keys():
3266 ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3267 for t in self.last_yticks.keys():
3268 ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3269
3270 miniticksd = []
3271 for t in self.last_xminiticks:
3272 miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
3273 for t in self.last_yminiticks:
3274 miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
3275
3276 return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
3277
3278
3279
3280 class XErrorBars:
3281 """Draws x error bars at a set of points. This is usually used
3282 before (under) a set of Dots at the same points.
3283
3284 XErrorBars(d, attribute=value)
3285
3286 d required list of (x,y,xerr...) points
3287 attribute=value pairs keyword list SVG attributes
3288
3289 If points in d have
3290
3291 * 3 elements, the third is the symmetric error bar
3292 * 4 elements, the third and fourth are the asymmetric lower and
3293 upper error bar. The third element should be negative,
3294 e.g. (5, 5, -1, 2) is a bar from 4 to 7.
3295 * more than 4, a tick mark is placed at each value. This lets
3296 you nest errors from different sources, correlated and
3297 uncorrelated, statistical and systematic, etc.
3298 """
3299 defaults = {"stroke-width":"0.25pt"}
3300
3301 def __repr__(self):
3302 return "<XErrorBars (%d nodes)>" % len(self.d)
3303
3304 def __init__(self, d=[], **attr):
3305 self.d = list(d)
3306
3307 self.attr = dict(self.defaults)
3308 self.attr.update(attr)
3309
3310 def SVG(self, trans=None):
3311 """Apply the transformation "trans" and return an SVG object."""
3312 if isinstance(trans, str): trans = totrans(trans)
3313
3314 output = SVG("g")
3315 for p in self.d:
3316 x, y = p[0], p[1]
3317
3318 if len(p) == 3: bars = [x - p[2], x + p[2]]
3319 else: bars = [x + pi for pi in p[2:]]
3320
3321 start, end = min(bars), max(bars)
3322 output.append(LineAxis(start, y, end, y, start, end, bars, False, False, **self.attr).SVG(trans))
3323
3324 return output
3325
3326 class YErrorBars:
3327 """Draws y error bars at a set of points. This is usually used
3328 before (under) a set of Dots at the same points.
3329
3330 YErrorBars(d, attribute=value)
3331
3332 d required list of (x,y,yerr...) points
3333 attribute=value pairs keyword list SVG attributes
3334
3335 If points in d have
3336
3337 * 3 elements, the third is the symmetric error bar
3338 * 4 elements, the third and fourth are the asymmetric lower and
3339 upper error bar. The third element should be negative,
3340 e.g. (5, 5, -1, 2) is a bar from 4 to 7.
3341 * more than 4, a tick mark is placed at each value. This lets
3342 you nest errors from different sources, correlated and
3343 uncorrelated, statistical and systematic, etc.
3344 """
3345 defaults = {"stroke-width":"0.25pt"}
3346
3347 def __repr__(self):
3348 return "<YErrorBars (%d nodes)>" % len(self.d)
3349
3350 def __init__(self, d=[], **attr):
3351 self.d = list(d)
3352
3353 self.attr = dict(self.defaults)
3354 self.attr.update(attr)
3355
3356 def SVG(self, trans=None):
3357 """Apply the transformation "trans" and return an SVG object."""
3358 if isinstance(trans, str): trans = totrans(trans)
3359
3360 output = SVG("g")
3361 for p in self.d:
3362 x, y = p[0], p[1]
3363
3364 if len(p) == 3: bars = [y - p[2], y + p[2]]
3365 else: bars = [y + pi for pi in p[2:]]
3366
3367 start, end = min(bars), max(bars)
3368 output.append(LineAxis(x, start, x, end, start, end, bars, False, False, **self.attr).SVG(trans))
3369
3370 return output