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