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