mirror of
https://github.com/opencv/opencv.git
synced 2024-12-27 03:14:05 +08:00
3684 lines
148 KiB
Python
Executable File
3684 lines
148 KiB
Python
Executable File
# svgfig.py copyright (C) 2008 Jim Pivarski <jpivarski@gmail.com>
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
#
|
|
# Full licence is in the file COPYING and at http://www.gnu.org/copyleft/gpl.html
|
|
|
|
import re, codecs, os, platform, copy, itertools, math, cmath, random, sys, copy
|
|
_epsilon = 1e-5
|
|
|
|
if sys.version_info >= (3,0):
|
|
long = int
|
|
basestring = (str,bytes)
|
|
|
|
# Fix Python 2.x.
|
|
try:
|
|
UNICODE_EXISTS = bool(type(unicode))
|
|
except NameError:
|
|
unicode = lambda s: str(s)
|
|
|
|
try:
|
|
xrange # Python 2
|
|
except NameError:
|
|
xrange = range # Python 3
|
|
|
|
|
|
if re.search("windows", platform.system(), re.I):
|
|
try:
|
|
import _winreg
|
|
_default_directory = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER,
|
|
r"Software\Microsoft\Windows\Current Version\Explorer\Shell Folders"), "Desktop")[0]
|
|
# tmpdir = _winreg.QueryValueEx(_winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Environment"), "TEMP")[0]
|
|
# if tmpdir[0:13] != "%USERPROFILE%":
|
|
# tmpdir = os.path.expanduser("~") + tmpdir[13:]
|
|
except:
|
|
_default_directory = os.path.expanduser("~") + os.sep + "Desktop"
|
|
|
|
_default_fileName = "tmp.svg"
|
|
|
|
_hacks = {}
|
|
_hacks["inkscape-text-vertical-shift"] = False
|
|
|
|
|
|
def rgb(r, g, b, maximum=1.):
|
|
"""Create an SVG color string "#xxyyzz" from r, g, and b.
|
|
|
|
r,g,b = 0 is black and r,g,b = maximum is white.
|
|
"""
|
|
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)))
|
|
|
|
def attr_preprocess(attr):
|
|
attrCopy = attr.copy()
|
|
for name in attr.keys():
|
|
name_colon = re.sub("__", ":", name)
|
|
if name_colon != name:
|
|
attrCopy[name_colon] = attrCopy[name]
|
|
del attrCopy[name]
|
|
name = name_colon
|
|
|
|
name_dash = re.sub("_", "-", name)
|
|
if name_dash != name:
|
|
attrCopy[name_dash] = attrCopy[name]
|
|
del attrCopy[name]
|
|
name = name_dash
|
|
|
|
return attrCopy
|
|
|
|
|
|
class SVG:
|
|
"""A tree representation of an SVG image or image fragment.
|
|
|
|
SVG(t, sub, sub, sub..., attribute=value)
|
|
|
|
t required SVG type name
|
|
sub optional list nested SVG elements or text/Unicode
|
|
attribute=value pairs optional keywords SVG attributes
|
|
|
|
In attribute names, "__" becomes ":" and "_" becomes "-".
|
|
|
|
SVG in XML
|
|
|
|
<g id="mygroup" fill="blue">
|
|
<rect x="1" y="1" width="2" height="2" />
|
|
<rect x="3" y="3" width="2" height="2" />
|
|
</g>
|
|
|
|
SVG in Python
|
|
|
|
>>> svg = SVG("g", SVG("rect", x=1, y=1, width=2, height=2), \
|
|
... SVG("rect", x=3, y=3, width=2, height=2), \
|
|
... id="mygroup", fill="blue")
|
|
|
|
Sub-elements and attributes may be accessed through tree-indexing:
|
|
|
|
>>> svg = SVG("text", SVG("tspan", "hello there"), stroke="none", fill="black")
|
|
>>> svg[0]
|
|
<tspan (1 sub) />
|
|
>>> svg[0, 0]
|
|
'hello there'
|
|
>>> svg["fill"]
|
|
'black'
|
|
|
|
Iteration is depth-first:
|
|
|
|
>>> svg = SVG("g", SVG("g", SVG("line", x1=0, y1=0, x2=1, y2=1)), \
|
|
... SVG("text", SVG("tspan", "hello again")))
|
|
...
|
|
>>> for ti, s in svg:
|
|
... print ti, repr(s)
|
|
...
|
|
(0,) <g (1 sub) />
|
|
(0, 0) <line x2=1 y1=0 x1=0 y2=1 />
|
|
(0, 0, 'x2') 1
|
|
(0, 0, 'y1') 0
|
|
(0, 0, 'x1') 0
|
|
(0, 0, 'y2') 1
|
|
(1,) <text (1 sub) />
|
|
(1, 0) <tspan (1 sub) />
|
|
(1, 0, 0) 'hello again'
|
|
|
|
Use "print" to navigate:
|
|
|
|
>>> print svg
|
|
None <g (2 sub) />
|
|
[0] <g (1 sub) />
|
|
[0, 0] <line x2=1 y1=0 x1=0 y2=1 />
|
|
[1] <text (1 sub) />
|
|
[1, 0] <tspan (1 sub) />
|
|
"""
|
|
def __init__(self, *t_sub, **attr):
|
|
if len(t_sub) == 0:
|
|
raise TypeError( "SVG element must have a t (SVG type)")
|
|
|
|
# first argument is t (SVG type)
|
|
self.t = t_sub[0]
|
|
# the rest are sub-elements
|
|
self.sub = list(t_sub[1:])
|
|
|
|
# keyword arguments are attributes
|
|
# need to preprocess to handle differences between SVG and Python syntax
|
|
self.attr = attr_preprocess(attr)
|
|
|
|
def __getitem__(self, ti):
|
|
"""Index is a list that descends tree, returning a sub-element if
|
|
it ends with a number and an attribute if it ends with a string."""
|
|
obj = self
|
|
if isinstance(ti, (list, tuple)):
|
|
for i in ti[:-1]:
|
|
obj = obj[i]
|
|
ti = ti[-1]
|
|
|
|
if isinstance(ti, (int, long, slice)):
|
|
return obj.sub[ti]
|
|
else:
|
|
return obj.attr[ti]
|
|
|
|
def __setitem__(self, ti, value):
|
|
"""Index is a list that descends tree, returning a sub-element if
|
|
it ends with a number and an attribute if it ends with a string."""
|
|
obj = self
|
|
if isinstance(ti, (list, tuple)):
|
|
for i in ti[:-1]:
|
|
obj = obj[i]
|
|
ti = ti[-1]
|
|
|
|
if isinstance(ti, (int, long, slice)):
|
|
obj.sub[ti] = value
|
|
else:
|
|
obj.attr[ti] = value
|
|
|
|
def __delitem__(self, ti):
|
|
"""Index is a list that descends tree, returning a sub-element if
|
|
it ends with a number and an attribute if it ends with a string."""
|
|
obj = self
|
|
if isinstance(ti, (list, tuple)):
|
|
for i in ti[:-1]:
|
|
obj = obj[i]
|
|
ti = ti[-1]
|
|
|
|
if isinstance(ti, (int, long, slice)):
|
|
del obj.sub[ti]
|
|
else:
|
|
del obj.attr[ti]
|
|
|
|
def __contains__(self, value):
|
|
"""x in svg == True iff x is an attribute in svg."""
|
|
return value in self.attr
|
|
|
|
def __eq__(self, other):
|
|
"""x == y iff x represents the same SVG as y."""
|
|
if id(self) == id(other):
|
|
return True
|
|
return (isinstance(other, SVG) and
|
|
self.t == other.t and self.sub == other.sub and self.attr == other.attr)
|
|
|
|
def __ne__(self, other):
|
|
"""x != y iff x does not represent the same SVG as y."""
|
|
return not (self == other)
|
|
|
|
def append(self, x):
|
|
"""Appends x to the list of sub-elements (drawn last, overlaps
|
|
other primitives)."""
|
|
self.sub.append(x)
|
|
|
|
def prepend(self, x):
|
|
"""Prepends x to the list of sub-elements (drawn first may be
|
|
overlapped by other primitives)."""
|
|
self.sub[0:0] = [x]
|
|
|
|
def extend(self, x):
|
|
"""Extends list of sub-elements by a list x."""
|
|
self.sub.extend(x)
|
|
|
|
def clone(self, shallow=False):
|
|
"""Deep copy of SVG tree. Set shallow=True for a shallow copy."""
|
|
if shallow:
|
|
return copy.copy(self)
|
|
else:
|
|
return copy.deepcopy(self)
|
|
|
|
### nested class
|
|
class SVGDepthIterator:
|
|
"""Manages SVG iteration."""
|
|
|
|
def __init__(self, svg, ti, depth_limit):
|
|
self.svg = svg
|
|
self.ti = ti
|
|
self.shown = False
|
|
self.depth_limit = depth_limit
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def next(self):
|
|
if not self.shown:
|
|
self.shown = True
|
|
if self.ti != ():
|
|
return self.ti, self.svg
|
|
|
|
if not isinstance(self.svg, SVG):
|
|
raise StopIteration
|
|
if self.depth_limit is not None and len(self.ti) >= self.depth_limit:
|
|
raise StopIteration
|
|
|
|
if "iterators" not in self.__dict__:
|
|
self.iterators = []
|
|
for i, s in enumerate(self.svg.sub):
|
|
self.iterators.append(self.__class__(s, self.ti + (i,), self.depth_limit))
|
|
for k, s in self.svg.attr.items():
|
|
self.iterators.append(self.__class__(s, self.ti + (k,), self.depth_limit))
|
|
self.iterators = itertools.chain(*self.iterators)
|
|
|
|
return self.iterators.next()
|
|
### end nested class
|
|
|
|
def depth_first(self, depth_limit=None):
|
|
"""Returns a depth-first generator over the SVG. If depth_limit
|
|
is a number, stop recursion at that depth."""
|
|
return self.SVGDepthIterator(self, (), depth_limit)
|
|
|
|
def breadth_first(self, depth_limit=None):
|
|
"""Not implemented yet. Any ideas on how to do it?
|
|
|
|
Returns a breadth-first generator over the SVG. If depth_limit
|
|
is a number, stop recursion at that depth."""
|
|
raise NotImplementedError( "Got an algorithm for breadth-first searching a tree without effectively copying the tree?")
|
|
|
|
def __iter__(self):
|
|
return self.depth_first()
|
|
|
|
def items(self, sub=True, attr=True, text=True):
|
|
"""Get a recursively-generated list of tree-index, sub-element/attribute pairs.
|
|
|
|
If sub == False, do not show sub-elements.
|
|
If attr == False, do not show attributes.
|
|
If text == False, do not show text/Unicode sub-elements.
|
|
"""
|
|
output = []
|
|
for ti, s in self:
|
|
show = False
|
|
if isinstance(ti[-1], (int, long)):
|
|
if isinstance(s, basestring):
|
|
show = text
|
|
else:
|
|
show = sub
|
|
else:
|
|
show = attr
|
|
|
|
if show:
|
|
output.append((ti, s))
|
|
return output
|
|
|
|
def keys(self, sub=True, attr=True, text=True):
|
|
"""Get a recursively-generated list of tree-indexes.
|
|
|
|
If sub == False, do not show sub-elements.
|
|
If attr == False, do not show attributes.
|
|
If text == False, do not show text/Unicode sub-elements.
|
|
"""
|
|
return [ti for ti, s in self.items(sub, attr, text)]
|
|
|
|
def values(self, sub=True, attr=True, text=True):
|
|
"""Get a recursively-generated list of sub-elements and attributes.
|
|
|
|
If sub == False, do not show sub-elements.
|
|
If attr == False, do not show attributes.
|
|
If text == False, do not show text/Unicode sub-elements.
|
|
"""
|
|
return [s for ti, s in self.items(sub, attr, text)]
|
|
|
|
def __repr__(self):
|
|
return self.xml(depth_limit=0)
|
|
|
|
def __str__(self):
|
|
"""Print (actually, return a string of) the tree in a form useful for browsing."""
|
|
return self.tree(sub=True, attr=False, text=False)
|
|
|
|
def tree(self, depth_limit=None, sub=True, attr=True, text=True, tree_width=20, obj_width=80):
|
|
"""Print (actually, return a string of) the tree in a form useful for browsing.
|
|
|
|
If depth_limit == a number, stop recursion at that depth.
|
|
If sub == False, do not show sub-elements.
|
|
If attr == False, do not show attributes.
|
|
If text == False, do not show text/Unicode sub-elements.
|
|
tree_width is the number of characters reserved for printing tree indexes.
|
|
obj_width is the number of characters reserved for printing sub-elements/attributes.
|
|
"""
|
|
output = []
|
|
|
|
line = "%s %s" % (("%%-%ds" % tree_width) % repr(None),
|
|
("%%-%ds" % obj_width) % (repr(self))[0:obj_width])
|
|
output.append(line)
|
|
|
|
for ti, s in self.depth_first(depth_limit):
|
|
show = False
|
|
if isinstance(ti[-1], (int, long)):
|
|
if isinstance(s, basestring):
|
|
show = text
|
|
else:
|
|
show = sub
|
|
else:
|
|
show = attr
|
|
|
|
if show:
|
|
line = "%s %s" % (("%%-%ds" % tree_width) % repr(list(ti)),
|
|
("%%-%ds" % obj_width) % (" "*len(ti) + repr(s))[0:obj_width])
|
|
output.append(line)
|
|
|
|
return "\n".join(output)
|
|
|
|
def xml(self, indent=u" ", newl=u"\n", depth_limit=None, depth=0):
|
|
"""Get an XML representation of the SVG.
|
|
|
|
indent string used for indenting
|
|
newl string used for newlines
|
|
If depth_limit == a number, stop recursion at that depth.
|
|
depth starting depth (not useful for users)
|
|
|
|
print svg.xml()
|
|
"""
|
|
attrstr = []
|
|
for n, v in self.attr.items():
|
|
if isinstance(v, dict):
|
|
v = u"; ".join([u"%s:%s" % (ni, vi) for ni, vi in v.items()])
|
|
elif isinstance(v, (list, tuple)):
|
|
v = u", ".join(v)
|
|
attrstr.append(u" %s=%s" % (n, repr(v)))
|
|
attrstr = u"".join(attrstr)
|
|
|
|
if len(self.sub) == 0:
|
|
return u"%s<%s%s />" % (indent * depth, self.t, attrstr)
|
|
|
|
if depth_limit is None or depth_limit > depth:
|
|
substr = []
|
|
for s in self.sub:
|
|
if isinstance(s, SVG):
|
|
substr.append(s.xml(indent, newl, depth_limit, depth + 1) + newl)
|
|
elif isinstance(s, basestring):
|
|
substr.append(u"%s%s%s" % (indent * (depth + 1), s, newl))
|
|
else:
|
|
substr.append("%s%s%s" % (indent * (depth + 1), repr(s), newl))
|
|
substr = u"".join(substr)
|
|
|
|
return u"%s<%s%s>%s%s%s</%s>" % (indent * depth, self.t, attrstr, newl, substr, indent * depth, self.t)
|
|
|
|
else:
|
|
return u"%s<%s (%d sub)%s />" % (indent * depth, self.t, len(self.sub), attrstr)
|
|
|
|
def standalone_xml(self, indent=u" ", newl=u"\n", encoding=u"utf-8"):
|
|
"""Get an XML representation of the SVG that can be saved/rendered.
|
|
|
|
indent string used for indenting
|
|
newl string used for newlines
|
|
"""
|
|
|
|
if self.t == "svg":
|
|
top = self
|
|
else:
|
|
top = canvas(self)
|
|
return u"""\
|
|
<?xml version="1.0" encoding="%s" standalone="no"?>
|
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
|
|
""" % encoding + (u"".join(top.__standalone_xml(indent, newl))) # end of return statement
|
|
|
|
def __standalone_xml(self, indent, newl):
|
|
output = [u"<%s" % self.t]
|
|
|
|
for n, v in self.attr.items():
|
|
if isinstance(v, dict):
|
|
v = u"; ".join([u"%s:%s" % (ni, vi) for ni, vi in v.items()])
|
|
elif isinstance(v, (list, tuple)):
|
|
v = u", ".join(v)
|
|
output.append(u' %s="%s"' % (n, v))
|
|
|
|
if len(self.sub) == 0:
|
|
output.append(u" />%s%s" % (newl, newl))
|
|
return output
|
|
|
|
elif self.t == "text" or self.t == "tspan" or self.t == "style":
|
|
output.append(u">")
|
|
|
|
else:
|
|
output.append(u">%s%s" % (newl, newl))
|
|
|
|
for s in self.sub:
|
|
if isinstance(s, SVG):
|
|
output.extend(s.__standalone_xml(indent, newl))
|
|
else:
|
|
output.append(unicode(s))
|
|
|
|
if self.t == "tspan":
|
|
output.append(u"</%s>" % self.t)
|
|
else:
|
|
output.append(u"</%s>%s%s" % (self.t, newl, newl))
|
|
|
|
return output
|
|
|
|
def interpret_fileName(self, fileName=None):
|
|
if fileName is None:
|
|
fileName = _default_fileName
|
|
if re.search("windows", platform.system(), re.I) and not os.path.isabs(fileName):
|
|
fileName = _default_directory + os.sep + fileName
|
|
return fileName
|
|
|
|
def save(self, fileName=None, encoding="utf-8", compresslevel=None):
|
|
"""Save to a file for viewing. Note that svg.save() overwrites the file named _default_fileName.
|
|
|
|
fileName default=None note that _default_fileName will be overwritten if
|
|
no fileName is specified. If the extension
|
|
is ".svgz" or ".gz", the output will be gzipped
|
|
encoding default="utf-8" file encoding
|
|
compresslevel default=None if a number, the output will be gzipped with that
|
|
compression level (1-9, 1 being fastest and 9 most
|
|
thorough)
|
|
"""
|
|
fileName = self.interpret_fileName(fileName)
|
|
|
|
if compresslevel is not None or re.search(r"\.svgz$", fileName, re.I) or re.search(r"\.gz$", fileName, re.I):
|
|
import gzip
|
|
if compresslevel is None:
|
|
f = gzip.GzipFile(fileName, "w")
|
|
else:
|
|
f = gzip.GzipFile(fileName, "w", compresslevel)
|
|
|
|
f = codecs.EncodedFile(f, "utf-8", encoding)
|
|
f.write(self.standalone_xml(encoding=encoding))
|
|
f.close()
|
|
|
|
else:
|
|
f = codecs.open(fileName, "w", encoding=encoding)
|
|
f.write(self.standalone_xml(encoding=encoding))
|
|
f.close()
|
|
|
|
def inkview(self, fileName=None, encoding="utf-8"):
|
|
"""View in "inkview", assuming that program is available on your system.
|
|
|
|
fileName default=None note that any file named _default_fileName will be
|
|
overwritten if no fileName is specified. If the extension
|
|
is ".svgz" or ".gz", the output will be gzipped
|
|
encoding default="utf-8" file encoding
|
|
"""
|
|
fileName = self.interpret_fileName(fileName)
|
|
self.save(fileName, encoding)
|
|
os.spawnvp(os.P_NOWAIT, "inkview", ("inkview", fileName))
|
|
|
|
def inkscape(self, fileName=None, encoding="utf-8"):
|
|
"""View in "inkscape", assuming that program is available on your system.
|
|
|
|
fileName default=None note that any file named _default_fileName will be
|
|
overwritten if no fileName is specified. If the extension
|
|
is ".svgz" or ".gz", the output will be gzipped
|
|
encoding default="utf-8" file encoding
|
|
"""
|
|
fileName = self.interpret_fileName(fileName)
|
|
self.save(fileName, encoding)
|
|
os.spawnvp(os.P_NOWAIT, "inkscape", ("inkscape", fileName))
|
|
|
|
def firefox(self, fileName=None, encoding="utf-8"):
|
|
"""View in "firefox", assuming that program is available on your system.
|
|
|
|
fileName default=None note that any file named _default_fileName will be
|
|
overwritten if no fileName is specified. If the extension
|
|
is ".svgz" or ".gz", the output will be gzipped
|
|
encoding default="utf-8" file encoding
|
|
"""
|
|
fileName = self.interpret_fileName(fileName)
|
|
self.save(fileName, encoding)
|
|
os.spawnvp(os.P_NOWAIT, "firefox", ("firefox", fileName))
|
|
|
|
######################################################################
|
|
|
|
_canvas_defaults = {"width": "400px",
|
|
"height": "400px",
|
|
"viewBox": "0 0 100 100",
|
|
"xmlns": "http://www.w3.org/2000/svg",
|
|
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
|
"version": "1.1",
|
|
"style": {"stroke": "black",
|
|
"fill": "none",
|
|
"stroke-width": "0.5pt",
|
|
"stroke-linejoin": "round",
|
|
"text-anchor": "middle",
|
|
},
|
|
"font-family": ["Helvetica", "Arial", "FreeSans", "Sans", "sans", "sans-serif"],
|
|
}
|
|
|
|
def canvas(*sub, **attr):
|
|
"""Creates a top-level SVG object, allowing the user to control the
|
|
image size and aspect ratio.
|
|
|
|
canvas(sub, sub, sub..., attribute=value)
|
|
|
|
sub optional list nested SVG elements or text/Unicode
|
|
attribute=value pairs optional keywords SVG attributes
|
|
|
|
Default attribute values:
|
|
|
|
width "400px"
|
|
height "400px"
|
|
viewBox "0 0 100 100"
|
|
xmlns "http://www.w3.org/2000/svg"
|
|
xmlns:xlink "http://www.w3.org/1999/xlink"
|
|
version "1.1"
|
|
style "stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoin:round; text-anchor:middle"
|
|
font-family "Helvetica,Arial,FreeSans?,Sans,sans,sans-serif"
|
|
"""
|
|
attributes = dict(_canvas_defaults)
|
|
attributes.update(attr)
|
|
|
|
if sub is None or sub == ():
|
|
return SVG("svg", **attributes)
|
|
else:
|
|
return SVG("svg", *sub, **attributes)
|
|
|
|
def canvas_outline(*sub, **attr):
|
|
"""Same as canvas(), but draws an outline around the drawable area,
|
|
so that you know how close your image is to the edges."""
|
|
svg = canvas(*sub, **attr)
|
|
match = re.match(r"[, \t]*([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]+([0-9e.+\-]+)[, \t]*", svg["viewBox"])
|
|
if match is None:
|
|
raise ValueError( "canvas viewBox is incorrectly formatted")
|
|
x, y, width, height = [float(x) for x in match.groups()]
|
|
svg.prepend(SVG("rect", x=x, y=y, width=width, height=height, stroke="none", fill="cornsilk"))
|
|
svg.append(SVG("rect", x=x, y=y, width=width, height=height, stroke="black", fill="none"))
|
|
return svg
|
|
|
|
def template(fileName, svg, replaceme="REPLACEME"):
|
|
"""Loads an SVG image from a file, replacing instances of
|
|
<REPLACEME /> with a given svg object.
|
|
|
|
fileName required name of the template SVG
|
|
svg required SVG object for replacement
|
|
replaceme default="REPLACEME" fake SVG element to be replaced by the given object
|
|
|
|
>>> print load("template.svg")
|
|
None <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
|
|
[0] <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
|
|
[1] <REPLACEME />
|
|
>>>
|
|
>>> print template("template.svg", SVG("circle", cx=50, cy=50, r=30))
|
|
None <svg (2 sub) style=u'stroke:black; fill:none; stroke-width:0.5pt; stroke-linejoi
|
|
[0] <rect height=u'100' width=u'100' stroke=u'none' y=u'0' x=u'0' fill=u'yellow'
|
|
[1] <circle cy=50 cx=50 r=30 />
|
|
"""
|
|
output = load(fileName)
|
|
for ti, s in output:
|
|
if isinstance(s, SVG) and s.t == replaceme:
|
|
output[ti] = svg
|
|
return output
|
|
|
|
######################################################################
|
|
|
|
def load(fileName):
|
|
"""Loads an SVG image from a file."""
|
|
return load_stream(open(fileName))
|
|
|
|
def load_stream(stream):
|
|
"""Loads an SVG image from a stream (can be a string or a file object)."""
|
|
|
|
from xml.sax import handler, make_parser
|
|
from xml.sax.handler import feature_namespaces, feature_external_ges, feature_external_pes
|
|
|
|
class ContentHandler(handler.ContentHandler):
|
|
def __init__(self):
|
|
self.stack = []
|
|
self.output = None
|
|
self.all_whitespace = re.compile(r"^\s*$")
|
|
|
|
def startElement(self, name, attr):
|
|
s = SVG(name)
|
|
s.attr = dict(attr.items())
|
|
if len(self.stack) > 0:
|
|
last = self.stack[-1]
|
|
last.sub.append(s)
|
|
self.stack.append(s)
|
|
|
|
def characters(self, ch):
|
|
if not isinstance(ch, basestring) or self.all_whitespace.match(ch) is None:
|
|
if len(self.stack) > 0:
|
|
last = self.stack[-1]
|
|
if len(last.sub) > 0 and isinstance(last.sub[-1], basestring):
|
|
last.sub[-1] = last.sub[-1] + "\n" + ch
|
|
else:
|
|
last.sub.append(ch)
|
|
|
|
def endElement(self, name):
|
|
if len(self.stack) > 0:
|
|
last = self.stack[-1]
|
|
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)):
|
|
last.sub[0] = "<![CDATA[\n" + last.sub[0] + "]]>"
|
|
|
|
self.output = self.stack.pop()
|
|
|
|
ch = ContentHandler()
|
|
parser = make_parser()
|
|
parser.setContentHandler(ch)
|
|
parser.setFeature(feature_namespaces, 0)
|
|
parser.setFeature(feature_external_ges, 0)
|
|
parser.parse(stream)
|
|
return ch.output
|
|
|
|
######################################################################
|
|
def set_func_name(f, name):
|
|
"""try to patch the function name string into a function object"""
|
|
try:
|
|
f.func_name = name
|
|
except TypeError:
|
|
# py 2.3 raises: TypeError: readonly attribute
|
|
pass
|
|
|
|
def totrans(expr, vars=("x", "y"), globals=None, locals=None):
|
|
"""Converts to a coordinate transformation (a function that accepts
|
|
two arguments and returns two values).
|
|
|
|
expr required a string expression or a function
|
|
of two real or one complex value
|
|
vars default=("x", "y") independent variable names; a singleton
|
|
("z",) is interpreted as complex
|
|
globals default=None dict of global variables
|
|
locals default=None dict of local variables
|
|
"""
|
|
if locals is None:
|
|
locals = {} # python 2.3's eval() won't accept None
|
|
|
|
if callable(expr):
|
|
if expr.func_code.co_argcount == 2:
|
|
return expr
|
|
|
|
elif expr.func_code.co_argcount == 1:
|
|
split = lambda z: (z.real, z.imag)
|
|
output = lambda x, y: split(expr(x + y*1j))
|
|
set_func_name(output, expr.func_name)
|
|
return output
|
|
|
|
else:
|
|
raise TypeError( "must be a function of 2 or 1 variables")
|
|
|
|
if len(vars) == 2:
|
|
g = math.__dict__
|
|
if globals is not None:
|
|
g.update(globals)
|
|
output = eval("lambda %s, %s: (%s)" % (vars[0], vars[1], expr), g, locals)
|
|
set_func_name(output, "%s,%s -> %s" % (vars[0], vars[1], expr))
|
|
return output
|
|
|
|
elif len(vars) == 1:
|
|
g = cmath.__dict__
|
|
if globals is not None:
|
|
g.update(globals)
|
|
output = eval("lambda %s: (%s)" % (vars[0], expr), g, locals)
|
|
split = lambda z: (z.real, z.imag)
|
|
output2 = lambda x, y: split(output(x + y*1j))
|
|
set_func_name(output2, "%s -> %s" % (vars[0], expr))
|
|
return output2
|
|
|
|
else:
|
|
raise TypeError( "vars must have 2 or 1 elements")
|
|
|
|
|
|
def window(xmin, xmax, ymin, ymax, x=0, y=0, width=100, height=100,
|
|
xlogbase=None, ylogbase=None, minusInfinity=-1000, flipx=False, flipy=True):
|
|
"""Creates and returns a coordinate transformation (a function that
|
|
accepts two arguments and returns two values) that transforms from
|
|
(xmin, ymin), (xmax, ymax)
|
|
to
|
|
(x, y), (x + width, y + height).
|
|
|
|
xlogbase, ylogbase default=None, None if a number, transform
|
|
logarithmically with given base
|
|
minusInfinity default=-1000 what to return if
|
|
log(0 or negative) is attempted
|
|
flipx default=False if true, reverse the direction of x
|
|
flipy default=True if true, reverse the direction of y
|
|
|
|
(When composing windows, be sure to set flipy=False.)
|
|
"""
|
|
|
|
if flipx:
|
|
ox1 = x + width
|
|
ox2 = x
|
|
else:
|
|
ox1 = x
|
|
ox2 = x + width
|
|
if flipy:
|
|
oy1 = y + height
|
|
oy2 = y
|
|
else:
|
|
oy1 = y
|
|
oy2 = y + height
|
|
ix1 = xmin
|
|
iy1 = ymin
|
|
ix2 = xmax
|
|
iy2 = ymax
|
|
|
|
if xlogbase is not None and (ix1 <= 0. or ix2 <= 0.):
|
|
raise ValueError ("x range incompatible with log scaling: (%g, %g)" % (ix1, ix2))
|
|
|
|
if ylogbase is not None and (iy1 <= 0. or iy2 <= 0.):
|
|
raise ValueError ("y range incompatible with log scaling: (%g, %g)" % (iy1, iy2))
|
|
|
|
def maybelog(t, it1, it2, ot1, ot2, logbase):
|
|
if t <= 0.:
|
|
return minusInfinity
|
|
else:
|
|
return ot1 + 1.*(math.log(t, logbase) - math.log(it1, logbase))/(math.log(it2, logbase) - math.log(it1, logbase)) * (ot2 - ot1)
|
|
|
|
xlogstr, ylogstr = "", ""
|
|
|
|
if xlogbase is None:
|
|
xfunc = lambda x: ox1 + 1.*(x - ix1)/(ix2 - ix1) * (ox2 - ox1)
|
|
else:
|
|
xfunc = lambda x: maybelog(x, ix1, ix2, ox1, ox2, xlogbase)
|
|
xlogstr = " xlog=%g" % xlogbase
|
|
|
|
if ylogbase is None:
|
|
yfunc = lambda y: oy1 + 1.*(y - iy1)/(iy2 - iy1) * (oy2 - oy1)
|
|
else:
|
|
yfunc = lambda y: maybelog(y, iy1, iy2, oy1, oy2, ylogbase)
|
|
ylogstr = " ylog=%g" % ylogbase
|
|
|
|
output = lambda x, y: (xfunc(x), yfunc(y))
|
|
|
|
set_func_name(output, "(%g, %g), (%g, %g) -> (%g, %g), (%g, %g)%s%s" % (
|
|
ix1, ix2, iy1, iy2, ox1, ox2, oy1, oy2, xlogstr, ylogstr))
|
|
return output
|
|
|
|
|
|
def rotate(angle, cx=0, cy=0):
|
|
"""Creates and returns a coordinate transformation which rotates
|
|
around (cx,cy) by "angle" degrees."""
|
|
angle *= math.pi/180.
|
|
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))
|
|
|
|
|
|
class Fig:
|
|
"""Stores graphics primitive objects and applies a single coordinate
|
|
transformation to them. To compose coordinate systems, nest Fig
|
|
objects.
|
|
|
|
Fig(obj, obj, obj..., trans=function)
|
|
|
|
obj optional list a list of drawing primitives
|
|
trans default=None a coordinate transformation function
|
|
|
|
>>> fig = Fig(Line(0,0,1,1), Rect(0.2,0.2,0.8,0.8), trans="2*x, 2*y")
|
|
>>> print fig.SVG().xml()
|
|
<g>
|
|
<path d='M0 0L2 2' />
|
|
<path d='M0.4 0.4L1.6 0.4ZL1.6 1.6ZL0.4 1.6ZL0.4 0.4ZZ' />
|
|
</g>
|
|
>>> print Fig(fig, trans="x/2., y/2.").SVG().xml()
|
|
<g>
|
|
<path d='M0 0L1 1' />
|
|
<path d='M0.2 0.2L0.8 0.2ZL0.8 0.8ZL0.2 0.8ZL0.2 0.2ZZ' />
|
|
</g>
|
|
"""
|
|
|
|
def __repr__(self):
|
|
if self.trans is None:
|
|
return "<Fig (%d items)>" % len(self.d)
|
|
elif isinstance(self.trans, basestring):
|
|
return "<Fig (%d items) x,y -> %s>" % (len(self.d), self.trans)
|
|
else:
|
|
return "<Fig (%d items) %s>" % (len(self.d), self.trans.func_name)
|
|
|
|
def __init__(self, *d, **kwds):
|
|
self.d = list(d)
|
|
defaults = {"trans": None, }
|
|
defaults.update(kwds)
|
|
kwds = defaults
|
|
|
|
self.trans = kwds["trans"]; del kwds["trans"]
|
|
if len(kwds) != 0:
|
|
raise TypeError ("Fig() got unexpected keyword arguments %s" % kwds.keys())
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object.
|
|
|
|
Coordinate transformations in nested Figs will be composed.
|
|
"""
|
|
|
|
if trans is None:
|
|
trans = self.trans
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans)
|
|
|
|
output = SVG("g")
|
|
for s in self.d:
|
|
if isinstance(s, SVG):
|
|
output.append(s)
|
|
|
|
elif isinstance(s, Fig):
|
|
strans = s.trans
|
|
if isinstance(strans, basestring):
|
|
strans = totrans(strans)
|
|
|
|
if trans is None:
|
|
subtrans = strans
|
|
elif strans is None:
|
|
subtrans = trans
|
|
else:
|
|
subtrans = lambda x, y: trans(*strans(x, y))
|
|
|
|
output.sub += s.SVG(subtrans).sub
|
|
|
|
elif s is None:
|
|
pass
|
|
|
|
else:
|
|
output.append(s.SVG(trans))
|
|
|
|
return output
|
|
|
|
|
|
class Plot:
|
|
"""Acts like Fig, but draws a coordinate axis. You also need to supply plot ranges.
|
|
|
|
Plot(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
|
|
|
|
xmin, xmax required minimum and maximum x values (in the objs' coordinates)
|
|
ymin, ymax required minimum and maximum y values (in the objs' coordinates)
|
|
obj optional list drawing primitives
|
|
keyword options keyword list options defined below
|
|
|
|
The following are keyword options, with their default values:
|
|
|
|
trans None transformation function
|
|
x, y 5, 5 upper-left corner of the Plot in SVG coordinates
|
|
width, height 90, 90 width and height of the Plot in SVG coordinates
|
|
flipx, flipy False, True flip the sign of the coordinate axis
|
|
minusInfinity -1000 if an axis is logarithmic and an object is plotted at 0 or
|
|
a negative value, -1000 will be used as a stand-in for NaN
|
|
atx, aty 0, 0 the place where the coordinate axes cross
|
|
xticks -10 request ticks according to the standard tick specification
|
|
(see help(Ticks))
|
|
xminiticks True request miniticks according to the standard minitick
|
|
specification
|
|
xlabels True request tick labels according to the standard tick label
|
|
specification
|
|
xlogbase None if a number, the axis and transformation are logarithmic
|
|
with ticks at the given base (10 being the most common)
|
|
(same for y)
|
|
arrows None if a new identifier, create arrow markers and draw them
|
|
at the ends of the coordinate axes
|
|
text_attr {} a dictionary of attributes for label text
|
|
axis_attr {} a dictionary of attributes for the axis lines
|
|
"""
|
|
|
|
def __repr__(self):
|
|
if self.trans is None:
|
|
return "<Plot (%d items)>" % len(self.d)
|
|
else:
|
|
return "<Plot (%d items) %s>" % (len(self.d), self.trans.func_name)
|
|
|
|
def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
|
|
self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
|
|
self.d = list(d)
|
|
defaults = {"trans": None,
|
|
"x": 5, "y": 5, "width": 90, "height": 90,
|
|
"flipx": False, "flipy": True,
|
|
"minusInfinity": -1000,
|
|
"atx": 0, "xticks": -10, "xminiticks": True, "xlabels": True, "xlogbase": None,
|
|
"aty": 0, "yticks": -10, "yminiticks": True, "ylabels": True, "ylogbase": None,
|
|
"arrows": None,
|
|
"text_attr": {}, "axis_attr": {},
|
|
}
|
|
defaults.update(kwds)
|
|
kwds = defaults
|
|
|
|
self.trans = kwds["trans"]; del kwds["trans"]
|
|
self.x = kwds["x"]; del kwds["x"]
|
|
self.y = kwds["y"]; del kwds["y"]
|
|
self.width = kwds["width"]; del kwds["width"]
|
|
self.height = kwds["height"]; del kwds["height"]
|
|
self.flipx = kwds["flipx"]; del kwds["flipx"]
|
|
self.flipy = kwds["flipy"]; del kwds["flipy"]
|
|
self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
|
|
self.atx = kwds["atx"]; del kwds["atx"]
|
|
self.xticks = kwds["xticks"]; del kwds["xticks"]
|
|
self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
|
|
self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
|
|
self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
|
|
self.aty = kwds["aty"]; del kwds["aty"]
|
|
self.yticks = kwds["yticks"]; del kwds["yticks"]
|
|
self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
|
|
self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
|
|
self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
|
|
self.arrows = kwds["arrows"]; del kwds["arrows"]
|
|
self.text_attr = kwds["text_attr"]; del kwds["text_attr"]
|
|
self.axis_attr = kwds["axis_attr"]; del kwds["axis_attr"]
|
|
if len(kwds) != 0:
|
|
raise TypeError ("Plot() got unexpected keyword arguments %s" % kwds.keys())
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
if trans is None:
|
|
trans = self.trans
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans)
|
|
|
|
self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax,
|
|
x=self.x, y=self.y, width=self.width, height=self.height,
|
|
xlogbase=self.xlogbase, ylogbase=self.ylogbase,
|
|
minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
|
|
|
|
d = ([Axes(self.xmin, self.xmax, self.ymin, self.ymax, self.atx, self.aty,
|
|
self.xticks, self.xminiticks, self.xlabels, self.xlogbase,
|
|
self.yticks, self.yminiticks, self.ylabels, self.ylogbase,
|
|
self.arrows, self.text_attr, **self.axis_attr)]
|
|
+ self.d)
|
|
|
|
return Fig(Fig(*d, **{"trans": trans})).SVG(self.last_window)
|
|
|
|
|
|
class Frame:
|
|
text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
|
|
axis_defaults = {}
|
|
|
|
tick_length = 1.5
|
|
minitick_length = 0.75
|
|
text_xaxis_offset = 1.
|
|
text_yaxis_offset = 2.
|
|
text_xtitle_offset = 6.
|
|
text_ytitle_offset = 12.
|
|
|
|
def __repr__(self):
|
|
return "<Frame (%d items)>" % len(self.d)
|
|
|
|
def __init__(self, xmin, xmax, ymin, ymax, *d, **kwds):
|
|
"""Acts like Fig, but draws a coordinate frame around the data. You also need to supply plot ranges.
|
|
|
|
Frame(xmin, xmax, ymin, ymax, obj, obj, obj..., keyword options...)
|
|
|
|
xmin, xmax required minimum and maximum x values (in the objs' coordinates)
|
|
ymin, ymax required minimum and maximum y values (in the objs' coordinates)
|
|
obj optional list drawing primitives
|
|
keyword options keyword list options defined below
|
|
|
|
The following are keyword options, with their default values:
|
|
|
|
x, y 20, 5 upper-left corner of the Frame in SVG coordinates
|
|
width, height 75, 80 width and height of the Frame in SVG coordinates
|
|
flipx, flipy False, True flip the sign of the coordinate axis
|
|
minusInfinity -1000 if an axis is logarithmic and an object is plotted at 0 or
|
|
a negative value, -1000 will be used as a stand-in for NaN
|
|
xtitle None if a string, label the x axis
|
|
xticks -10 request ticks according to the standard tick specification
|
|
(see help(Ticks))
|
|
xminiticks True request miniticks according to the standard minitick
|
|
specification
|
|
xlabels True request tick labels according to the standard tick label
|
|
specification
|
|
xlogbase None if a number, the axis and transformation are logarithmic
|
|
with ticks at the given base (10 being the most common)
|
|
(same for y)
|
|
text_attr {} a dictionary of attributes for label text
|
|
axis_attr {} a dictionary of attributes for the axis lines
|
|
"""
|
|
|
|
self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
|
|
self.d = list(d)
|
|
defaults = {"x": 20, "y": 5, "width": 75, "height": 80,
|
|
"flipx": False, "flipy": True, "minusInfinity": -1000,
|
|
"xtitle": None, "xticks": -10, "xminiticks": True, "xlabels": True,
|
|
"x2labels": None, "xlogbase": None,
|
|
"ytitle": None, "yticks": -10, "yminiticks": True, "ylabels": True,
|
|
"y2labels": None, "ylogbase": None,
|
|
"text_attr": {}, "axis_attr": {},
|
|
}
|
|
defaults.update(kwds)
|
|
kwds = defaults
|
|
|
|
self.x = kwds["x"]; del kwds["x"]
|
|
self.y = kwds["y"]; del kwds["y"]
|
|
self.width = kwds["width"]; del kwds["width"]
|
|
self.height = kwds["height"]; del kwds["height"]
|
|
self.flipx = kwds["flipx"]; del kwds["flipx"]
|
|
self.flipy = kwds["flipy"]; del kwds["flipy"]
|
|
self.minusInfinity = kwds["minusInfinity"]; del kwds["minusInfinity"]
|
|
self.xtitle = kwds["xtitle"]; del kwds["xtitle"]
|
|
self.xticks = kwds["xticks"]; del kwds["xticks"]
|
|
self.xminiticks = kwds["xminiticks"]; del kwds["xminiticks"]
|
|
self.xlabels = kwds["xlabels"]; del kwds["xlabels"]
|
|
self.x2labels = kwds["x2labels"]; del kwds["x2labels"]
|
|
self.xlogbase = kwds["xlogbase"]; del kwds["xlogbase"]
|
|
self.ytitle = kwds["ytitle"]; del kwds["ytitle"]
|
|
self.yticks = kwds["yticks"]; del kwds["yticks"]
|
|
self.yminiticks = kwds["yminiticks"]; del kwds["yminiticks"]
|
|
self.ylabels = kwds["ylabels"]; del kwds["ylabels"]
|
|
self.y2labels = kwds["y2labels"]; del kwds["y2labels"]
|
|
self.ylogbase = kwds["ylogbase"]; del kwds["ylogbase"]
|
|
|
|
self.text_attr = dict(self.text_defaults)
|
|
self.text_attr.update(kwds["text_attr"]); del kwds["text_attr"]
|
|
|
|
self.axis_attr = dict(self.axis_defaults)
|
|
self.axis_attr.update(kwds["axis_attr"]); del kwds["axis_attr"]
|
|
|
|
if len(kwds) != 0:
|
|
raise TypeError( "Frame() got unexpected keyword arguments %s" % kwds.keys())
|
|
|
|
def SVG(self):
|
|
"""Apply the window transformation and return an SVG object."""
|
|
|
|
self.last_window = window(self.xmin, self.xmax, self.ymin, self.ymax,
|
|
x=self.x, y=self.y, width=self.width, height=self.height,
|
|
xlogbase=self.xlogbase, ylogbase=self.ylogbase,
|
|
minusInfinity=self.minusInfinity, flipx=self.flipx, flipy=self.flipy)
|
|
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
|
|
left.tick_start = -self.tick_length
|
|
left.tick_end = 0
|
|
left.minitick_start = -self.minitick_length
|
|
left.minitick_end = 0.
|
|
left.text_start = self.text_yaxis_offset
|
|
|
|
right.tick_start = 0.
|
|
right.tick_end = self.tick_length
|
|
right.minitick_start = 0.
|
|
right.minitick_end = self.minitick_length
|
|
right.text_start = -self.text_yaxis_offset
|
|
right.text_attr["text-anchor"] = "start"
|
|
|
|
bottom.tick_start = 0.
|
|
bottom.tick_end = self.tick_length
|
|
bottom.minitick_start = 0.
|
|
bottom.minitick_end = self.minitick_length
|
|
bottom.text_start = -self.text_xaxis_offset
|
|
|
|
top.tick_start = -self.tick_length
|
|
top.tick_end = 0.
|
|
top.minitick_start = -self.minitick_length
|
|
top.minitick_end = 0.
|
|
top.text_start = self.text_xaxis_offset
|
|
top.text_attr["dominant-baseline"] = "text-after-edge"
|
|
|
|
output = Fig(*self.d).SVG(self.last_window)
|
|
output.prepend(left.SVG(self.last_window))
|
|
output.prepend(bottom.SVG(self.last_window))
|
|
output.prepend(right.SVG(self.last_window))
|
|
output.prepend(top.SVG(self.last_window))
|
|
|
|
if self.xtitle is not None:
|
|
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))
|
|
if self.ytitle is not None:
|
|
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))
|
|
return output
|
|
|
|
######################################################################
|
|
|
|
def pathtoPath(svg):
|
|
"""Converts SVG("path", d="...") into Path(d=[...])."""
|
|
if not isinstance(svg, SVG) or svg.t != "path":
|
|
raise TypeError ("Only SVG <path /> objects can be converted into Paths")
|
|
attr = dict(svg.attr)
|
|
d = attr["d"]
|
|
del attr["d"]
|
|
for key in attr.keys():
|
|
if not isinstance(key, str):
|
|
value = attr[key]
|
|
del attr[key]
|
|
attr[str(key)] = value
|
|
return Path(d, **attr)
|
|
|
|
|
|
class Path:
|
|
"""Path represents an SVG path, an arbitrary set of curves and
|
|
straight segments. Unlike SVG("path", d="..."), Path stores
|
|
coordinates as a list of numbers, rather than a string, so that it is
|
|
transformable in a Fig.
|
|
|
|
Path(d, attribute=value)
|
|
|
|
d required path data
|
|
attribute=value pairs keyword list SVG attributes
|
|
|
|
See http://www.w3.org/TR/SVG/paths.html for specification of paths
|
|
from text.
|
|
|
|
Internally, Path data is a list of tuples with these definitions:
|
|
|
|
* ("Z/z",): close the current path
|
|
* ("H/h", x) or ("V/v", y): a horizontal or vertical line
|
|
segment to x or y
|
|
* ("M/m/L/l/T/t", x, y, global): moveto, lineto, or smooth
|
|
quadratic curveto point (x, y). If global=True, (x, y) should
|
|
not be transformed.
|
|
* ("S/sQ/q", cx, cy, cglobal, x, y, global): polybezier or
|
|
smooth quadratic curveto point (x, y) using (cx, cy) as a
|
|
control point. If cglobal or global=True, (cx, cy) or (x, y)
|
|
should not be transformed.
|
|
* ("C/c", c1x, c1y, c1global, c2x, c2y, c2global, x, y, global):
|
|
cubic curveto point (x, y) using (c1x, c1y) and (c2x, c2y) as
|
|
control points. If c1global, c2global, or global=True, (c1x, c1y),
|
|
(c2x, c2y), or (x, y) should not be transformed.
|
|
* ("A/a", rx, ry, rglobal, x-axis-rotation, angle, large-arc-flag,
|
|
sweep-flag, x, y, global): arcto point (x, y) using the
|
|
aforementioned parameters.
|
|
* (",/.", rx, ry, rglobal, angle, x, y, global): an ellipse at
|
|
point (x, y) with radii (rx, ry). If angle is 0, the whole
|
|
ellipse is drawn; otherwise, a partial ellipse is drawn.
|
|
"""
|
|
defaults = {}
|
|
|
|
def __repr__(self):
|
|
return "<Path (%d nodes) %s>" % (len(self.d), self.attr)
|
|
|
|
def __init__(self, d=[], **attr):
|
|
if isinstance(d, basestring):
|
|
self.d = self.parse(d)
|
|
else:
|
|
self.d = list(d)
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def parse_whitespace(self, index, pathdata):
|
|
"""Part of Path's text-command parsing algorithm; used internally."""
|
|
while index < len(pathdata) and pathdata[index] in (" ", "\t", "\r", "\n", ","):
|
|
index += 1
|
|
return index, pathdata
|
|
|
|
def parse_command(self, index, pathdata):
|
|
"""Part of Path's text-command parsing algorithm; used internally."""
|
|
index, pathdata = self.parse_whitespace(index, pathdata)
|
|
|
|
if index >= len(pathdata):
|
|
return None, index, pathdata
|
|
command = pathdata[index]
|
|
if "A" <= command <= "Z" or "a" <= command <= "z":
|
|
index += 1
|
|
return command, index, pathdata
|
|
else:
|
|
return None, index, pathdata
|
|
|
|
def parse_number(self, index, pathdata):
|
|
"""Part of Path's text-command parsing algorithm; used internally."""
|
|
index, pathdata = self.parse_whitespace(index, pathdata)
|
|
|
|
if index >= len(pathdata):
|
|
return None, index, pathdata
|
|
first_digit = pathdata[index]
|
|
|
|
if "0" <= first_digit <= "9" or first_digit in ("-", "+", "."):
|
|
start = index
|
|
while index < len(pathdata) and ("0" <= pathdata[index] <= "9" or pathdata[index] in ("-", "+", ".", "e", "E")):
|
|
index += 1
|
|
end = index
|
|
|
|
index = end
|
|
return float(pathdata[start:end]), index, pathdata
|
|
else:
|
|
return None, index, pathdata
|
|
|
|
def parse_boolean(self, index, pathdata):
|
|
"""Part of Path's text-command parsing algorithm; used internally."""
|
|
index, pathdata = self.parse_whitespace(index, pathdata)
|
|
|
|
if index >= len(pathdata):
|
|
return None, index, pathdata
|
|
first_digit = pathdata[index]
|
|
|
|
if first_digit in ("0", "1"):
|
|
index += 1
|
|
return int(first_digit), index, pathdata
|
|
else:
|
|
return None, index, pathdata
|
|
|
|
def parse(self, pathdata):
|
|
"""Parses text-commands, converting them into a list of tuples.
|
|
Called by the constructor."""
|
|
output = []
|
|
index = 0
|
|
while True:
|
|
command, index, pathdata = self.parse_command(index, pathdata)
|
|
index, pathdata = self.parse_whitespace(index, pathdata)
|
|
|
|
if command is None and index == len(pathdata):
|
|
break # this is the normal way out of the loop
|
|
if command in ("Z", "z"):
|
|
output.append((command,))
|
|
|
|
######################
|
|
elif command in ("H", "h", "V", "v"):
|
|
errstring = "Path command \"%s\" requires a number at index %d" % (command, index)
|
|
num1, index, pathdata = self.parse_number(index, pathdata)
|
|
if num1 is None:
|
|
raise ValueError ( errstring)
|
|
|
|
while num1 is not None:
|
|
output.append((command, num1))
|
|
num1, index, pathdata = self.parse_number(index, pathdata)
|
|
|
|
######################
|
|
elif command in ("M", "m", "L", "l", "T", "t"):
|
|
errstring = "Path command \"%s\" requires an x,y pair at index %d" % (command, index)
|
|
num1, index, pathdata = self.parse_number(index, pathdata)
|
|
num2, index, pathdata = self.parse_number(index, pathdata)
|
|
|
|
if num1 is None:
|
|
raise ValueError ( errstring)
|
|
|
|
while num1 is not None:
|
|
if num2 is None:
|
|
raise ValueError ( errstring)
|
|
output.append((command, num1, num2, False))
|
|
|
|
num1, index, pathdata = self.parse_number(index, pathdata)
|
|
num2, index, pathdata = self.parse_number(index, pathdata)
|
|
|
|
######################
|
|
elif command in ("S", "s", "Q", "q"):
|
|
errstring = "Path command \"%s\" requires a cx,cy,x,y quadruplet at index %d" % (command, index)
|
|
num1, index, pathdata = self.parse_number(index, pathdata)
|
|
num2, index, pathdata = self.parse_number(index, pathdata)
|
|
num3, index, pathdata = self.parse_number(index, pathdata)
|
|
num4, index, pathdata = self.parse_number(index, pathdata)
|
|
|
|
if num1 is None:
|
|
raise ValueError ( errstring )
|
|
|
|
while num1 is not None:
|
|
if num2 is None or num3 is None or num4 is None:
|
|
raise ValueError (errstring)
|
|
output.append((command, num1, num2, False, num3, num4, False))
|
|
|
|
num1, index, pathdata = self.parse_number(index, pathdata)
|
|
num2, index, pathdata = self.parse_number(index, pathdata)
|
|
num3, index, pathdata = self.parse_number(index, pathdata)
|
|
num4, index, pathdata = self.parse_number(index, pathdata)
|
|
|
|
######################
|
|
elif command in ("C", "c"):
|
|
errstring = "Path command \"%s\" requires a c1x,c1y,c2x,c2y,x,y sextuplet at index %d" % (command, index)
|
|
num1, index, pathdata = self.parse_number(index, pathdata)
|
|
num2, index, pathdata = self.parse_number(index, pathdata)
|
|
num3, index, pathdata = self.parse_number(index, pathdata)
|
|
num4, index, pathdata = self.parse_number(index, pathdata)
|
|
num5, index, pathdata = self.parse_number(index, pathdata)
|
|
num6, index, pathdata = self.parse_number(index, pathdata)
|
|
|
|
if num1 is None:
|
|
raise ValueError(errstring)
|
|
|
|
while num1 is not None:
|
|
if num2 is None or num3 is None or num4 is None or num5 is None or num6 is None:
|
|
raise ValueError(errstring)
|
|
|
|
output.append((command, num1, num2, False, num3, num4, False, num5, num6, False))
|
|
|
|
num1, index, pathdata = self.parse_number(index, pathdata)
|
|
num2, index, pathdata = self.parse_number(index, pathdata)
|
|
num3, index, pathdata = self.parse_number(index, pathdata)
|
|
num4, index, pathdata = self.parse_number(index, pathdata)
|
|
num5, index, pathdata = self.parse_number(index, pathdata)
|
|
num6, index, pathdata = self.parse_number(index, pathdata)
|
|
|
|
######################
|
|
elif command in ("A", "a"):
|
|
errstring = "Path command \"%s\" requires a rx,ry,angle,large-arc-flag,sweep-flag,x,y septuplet at index %d" % (command, index)
|
|
num1, index, pathdata = self.parse_number(index, pathdata)
|
|
num2, index, pathdata = self.parse_number(index, pathdata)
|
|
num3, index, pathdata = self.parse_number(index, pathdata)
|
|
num4, index, pathdata = self.parse_boolean(index, pathdata)
|
|
num5, index, pathdata = self.parse_boolean(index, pathdata)
|
|
num6, index, pathdata = self.parse_number(index, pathdata)
|
|
num7, index, pathdata = self.parse_number(index, pathdata)
|
|
|
|
if num1 is None:
|
|
raise ValueError(errstring)
|
|
|
|
while num1 is not None:
|
|
if num2 is None or num3 is None or num4 is None or num5 is None or num6 is None or num7 is None:
|
|
raise ValueError(errstring)
|
|
|
|
output.append((command, num1, num2, False, num3, num4, num5, num6, num7, False))
|
|
|
|
num1, index, pathdata = self.parse_number(index, pathdata)
|
|
num2, index, pathdata = self.parse_number(index, pathdata)
|
|
num3, index, pathdata = self.parse_number(index, pathdata)
|
|
num4, index, pathdata = self.parse_boolean(index, pathdata)
|
|
num5, index, pathdata = self.parse_boolean(index, pathdata)
|
|
num6, index, pathdata = self.parse_number(index, pathdata)
|
|
num7, index, pathdata = self.parse_number(index, pathdata)
|
|
|
|
return output
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans)
|
|
|
|
x, y, X, Y = None, None, None, None
|
|
output = []
|
|
for datum in self.d:
|
|
if not isinstance(datum, (tuple, list)):
|
|
raise TypeError("pathdata elements must be tuples/lists")
|
|
|
|
command = datum[0]
|
|
|
|
######################
|
|
if command in ("Z", "z"):
|
|
x, y, X, Y = None, None, None, None
|
|
output.append("Z")
|
|
|
|
######################
|
|
elif command in ("H", "h", "V", "v"):
|
|
command, num1 = datum
|
|
|
|
if command == "H" or (command == "h" and x is None):
|
|
x = num1
|
|
elif command == "h":
|
|
x += num1
|
|
elif command == "V" or (command == "v" and y is None):
|
|
y = num1
|
|
elif command == "v":
|
|
y += num1
|
|
|
|
if trans is None:
|
|
X, Y = x, y
|
|
else:
|
|
X, Y = trans(x, y)
|
|
|
|
output.append("L%g %g" % (X, Y))
|
|
|
|
######################
|
|
elif command in ("M", "m", "L", "l", "T", "t"):
|
|
command, num1, num2, isglobal12 = datum
|
|
|
|
if trans is None or isglobal12:
|
|
if command.isupper() or X is None or Y is None:
|
|
X, Y = num1, num2
|
|
else:
|
|
X += num1
|
|
Y += num2
|
|
x, y = X, Y
|
|
|
|
else:
|
|
if command.isupper() or x is None or y is None:
|
|
x, y = num1, num2
|
|
else:
|
|
x += num1
|
|
y += num2
|
|
X, Y = trans(x, y)
|
|
|
|
COMMAND = command.capitalize()
|
|
output.append("%s%g %g" % (COMMAND, X, Y))
|
|
|
|
######################
|
|
elif command in ("S", "s", "Q", "q"):
|
|
command, num1, num2, isglobal12, num3, num4, isglobal34 = datum
|
|
|
|
if trans is None or isglobal12:
|
|
if command.isupper() or X is None or Y is None:
|
|
CX, CY = num1, num2
|
|
else:
|
|
CX = X + num1
|
|
CY = Y + num2
|
|
|
|
else:
|
|
if command.isupper() or x is None or y is None:
|
|
cx, cy = num1, num2
|
|
else:
|
|
cx = x + num1
|
|
cy = y + num2
|
|
CX, CY = trans(cx, cy)
|
|
|
|
if trans is None or isglobal34:
|
|
if command.isupper() or X is None or Y is None:
|
|
X, Y = num3, num4
|
|
else:
|
|
X += num3
|
|
Y += num4
|
|
x, y = X, Y
|
|
|
|
else:
|
|
if command.isupper() or x is None or y is None:
|
|
x, y = num3, num4
|
|
else:
|
|
x += num3
|
|
y += num4
|
|
X, Y = trans(x, y)
|
|
|
|
COMMAND = command.capitalize()
|
|
output.append("%s%g %g %g %g" % (COMMAND, CX, CY, X, Y))
|
|
|
|
######################
|
|
elif command in ("C", "c"):
|
|
command, num1, num2, isglobal12, num3, num4, isglobal34, num5, num6, isglobal56 = datum
|
|
|
|
if trans is None or isglobal12:
|
|
if command.isupper() or X is None or Y is None:
|
|
C1X, C1Y = num1, num2
|
|
else:
|
|
C1X = X + num1
|
|
C1Y = Y + num2
|
|
|
|
else:
|
|
if command.isupper() or x is None or y is None:
|
|
c1x, c1y = num1, num2
|
|
else:
|
|
c1x = x + num1
|
|
c1y = y + num2
|
|
C1X, C1Y = trans(c1x, c1y)
|
|
|
|
if trans is None or isglobal34:
|
|
if command.isupper() or X is None or Y is None:
|
|
C2X, C2Y = num3, num4
|
|
else:
|
|
C2X = X + num3
|
|
C2Y = Y + num4
|
|
|
|
else:
|
|
if command.isupper() or x is None or y is None:
|
|
c2x, c2y = num3, num4
|
|
else:
|
|
c2x = x + num3
|
|
c2y = y + num4
|
|
C2X, C2Y = trans(c2x, c2y)
|
|
|
|
if trans is None or isglobal56:
|
|
if command.isupper() or X is None or Y is None:
|
|
X, Y = num5, num6
|
|
else:
|
|
X += num5
|
|
Y += num6
|
|
x, y = X, Y
|
|
|
|
else:
|
|
if command.isupper() or x is None or y is None:
|
|
x, y = num5, num6
|
|
else:
|
|
x += num5
|
|
y += num6
|
|
X, Y = trans(x, y)
|
|
|
|
COMMAND = command.capitalize()
|
|
output.append("%s%g %g %g %g %g %g" % (COMMAND, C1X, C1Y, C2X, C2Y, X, Y))
|
|
|
|
######################
|
|
elif command in ("A", "a"):
|
|
command, num1, num2, isglobal12, angle, large_arc_flag, sweep_flag, num3, num4, isglobal34 = datum
|
|
|
|
oldx, oldy = x, y
|
|
OLDX, OLDY = X, Y
|
|
|
|
if trans is None or isglobal34:
|
|
if command.isupper() or X is None or Y is None:
|
|
X, Y = num3, num4
|
|
else:
|
|
X += num3
|
|
Y += num4
|
|
x, y = X, Y
|
|
|
|
else:
|
|
if command.isupper() or x is None or y is None:
|
|
x, y = num3, num4
|
|
else:
|
|
x += num3
|
|
y += num4
|
|
X, Y = trans(x, y)
|
|
|
|
if x is not None and y is not None:
|
|
centerx, centery = (x + oldx)/2., (y + oldy)/2.
|
|
CENTERX, CENTERY = (X + OLDX)/2., (Y + OLDY)/2.
|
|
|
|
if trans is None or isglobal12:
|
|
RX = CENTERX + num1
|
|
RY = CENTERY + num2
|
|
|
|
else:
|
|
rx = centerx + num1
|
|
ry = centery + num2
|
|
RX, RY = trans(rx, ry)
|
|
|
|
COMMAND = command.capitalize()
|
|
output.append("%s%g %g %g %d %d %g %g" % (COMMAND, RX - CENTERX, RY - CENTERY, angle, large_arc_flag, sweep_flag, X, Y))
|
|
|
|
elif command in (",", "."):
|
|
command, num1, num2, isglobal12, angle, num3, num4, isglobal34 = datum
|
|
if trans is None or isglobal34:
|
|
if command == "." or X is None or Y is None:
|
|
X, Y = num3, num4
|
|
else:
|
|
X += num3
|
|
Y += num4
|
|
x, y = None, None
|
|
|
|
else:
|
|
if command == "." or x is None or y is None:
|
|
x, y = num3, num4
|
|
else:
|
|
x += num3
|
|
y += num4
|
|
X, Y = trans(x, y)
|
|
|
|
if trans is None or isglobal12:
|
|
RX = X + num1
|
|
RY = Y + num2
|
|
|
|
else:
|
|
rx = x + num1
|
|
ry = y + num2
|
|
RX, RY = trans(rx, ry)
|
|
|
|
RX, RY = RX - X, RY - Y
|
|
|
|
X1, Y1 = X + RX * math.cos(angle*math.pi/180.), Y + RX * math.sin(angle*math.pi/180.)
|
|
X2, Y2 = X + RY * math.sin(angle*math.pi/180.), Y - RY * math.cos(angle*math.pi/180.)
|
|
X3, Y3 = X - RX * math.cos(angle*math.pi/180.), Y - RX * math.sin(angle*math.pi/180.)
|
|
X4, Y4 = X - RY * math.sin(angle*math.pi/180.), Y + RY * math.cos(angle*math.pi/180.)
|
|
|
|
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" % (
|
|
X1, Y1, RX, RY, angle, X2, Y2, RX, RY, angle, X3, Y3, RX, RY, angle, X4, Y4, RX, RY, angle, X1, Y1))
|
|
|
|
return SVG("path", d="".join(output), **self.attr)
|
|
|
|
######################################################################
|
|
|
|
def funcRtoC(expr, var="t", globals=None, locals=None):
|
|
"""Converts a complex "z(t)" string to a function acceptable for Curve.
|
|
|
|
expr required string in the form "z(t)"
|
|
var default="t" name of the independent variable
|
|
globals default=None dict of global variables used in the expression;
|
|
you may want to use Python's builtin globals()
|
|
locals default=None dict of local variables
|
|
"""
|
|
if locals is None:
|
|
locals = {} # python 2.3's eval() won't accept None
|
|
g = cmath.__dict__
|
|
if globals is not None:
|
|
g.update(globals)
|
|
output = eval("lambda %s: (%s)" % (var, expr), g, locals)
|
|
split = lambda z: (z.real, z.imag)
|
|
output2 = lambda t: split(output(t))
|
|
set_func_name(output2, "%s -> %s" % (var, expr))
|
|
return output2
|
|
|
|
|
|
def funcRtoR2(expr, var="t", globals=None, locals=None):
|
|
"""Converts a "f(t), g(t)" string to a function acceptable for Curve.
|
|
|
|
expr required string in the form "f(t), g(t)"
|
|
var default="t" name of the independent variable
|
|
globals default=None dict of global variables used in the expression;
|
|
you may want to use Python's builtin globals()
|
|
locals default=None dict of local variables
|
|
"""
|
|
if locals is None:
|
|
locals = {} # python 2.3's eval() won't accept None
|
|
g = math.__dict__
|
|
if globals is not None:
|
|
g.update(globals)
|
|
output = eval("lambda %s: (%s)" % (var, expr), g, locals)
|
|
set_func_name(output, "%s -> %s" % (var, expr))
|
|
return output
|
|
|
|
|
|
def funcRtoR(expr, var="x", globals=None, locals=None):
|
|
"""Converts a "f(x)" string to a function acceptable for Curve.
|
|
|
|
expr required string in the form "f(x)"
|
|
var default="x" name of the independent variable
|
|
globals default=None dict of global variables used in the expression;
|
|
you may want to use Python's builtin globals()
|
|
locals default=None dict of local variables
|
|
"""
|
|
if locals is None:
|
|
locals = {} # python 2.3's eval() won't accept None
|
|
g = math.__dict__
|
|
if globals is not None:
|
|
g.update(globals)
|
|
output = eval("lambda %s: (%s, %s)" % (var, var, expr), g, locals)
|
|
set_func_name(output, "%s -> %s" % (var, expr))
|
|
return output
|
|
|
|
|
|
class Curve:
|
|
"""Draws a parametric function as a path.
|
|
|
|
Curve(f, low, high, loop, attribute=value)
|
|
|
|
f required a Python callable or string in
|
|
the form "f(t), g(t)"
|
|
low, high required left and right endpoints
|
|
loop default=False if True, connect the endpoints
|
|
attribute=value pairs keyword list SVG attributes
|
|
"""
|
|
defaults = {}
|
|
random_sampling = True
|
|
recursion_limit = 15
|
|
linearity_limit = 0.05
|
|
discontinuity_limit = 5.
|
|
|
|
def __repr__(self):
|
|
return "<Curve %s [%s, %s] %s>" % (self.f, self.low, self.high, self.attr)
|
|
|
|
def __init__(self, f, low, high, loop=False, **attr):
|
|
self.f = f
|
|
self.low = low
|
|
self.high = high
|
|
self.loop = loop
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
### nested class Sample
|
|
class Sample:
|
|
def __repr__(self):
|
|
t, x, y, X, Y = self.t, self.x, self.y, self.X, self.Y
|
|
if t is not None:
|
|
t = "%g" % t
|
|
if x is not None:
|
|
x = "%g" % x
|
|
if y is not None:
|
|
y = "%g" % y
|
|
if X is not None:
|
|
X = "%g" % X
|
|
if Y is not None:
|
|
Y = "%g" % Y
|
|
return "<Curve.Sample t=%s x=%s y=%s X=%s Y=%s>" % (t, x, y, X, Y)
|
|
|
|
def __init__(self, t):
|
|
self.t = t
|
|
|
|
def link(self, left, right):
|
|
self.left, self.right = left, right
|
|
|
|
def evaluate(self, f, trans):
|
|
self.x, self.y = f(self.t)
|
|
if trans is None:
|
|
self.X, self.Y = self.x, self.y
|
|
else:
|
|
self.X, self.Y = trans(self.x, self.y)
|
|
### end Sample
|
|
|
|
### nested class Samples
|
|
class Samples:
|
|
def __repr__(self):
|
|
return "<Curve.Samples (%d samples)>" % len(self)
|
|
|
|
def __init__(self, left, right):
|
|
self.left, self.right = left, right
|
|
|
|
def __len__(self):
|
|
count = 0
|
|
current = self.left
|
|
while current is not None:
|
|
count += 1
|
|
current = current.right
|
|
return count
|
|
|
|
def __iter__(self):
|
|
self.current = self.left
|
|
return self
|
|
|
|
def next(self):
|
|
current = self.current
|
|
if current is None:
|
|
raise StopIteration
|
|
self.current = self.current.right
|
|
return current
|
|
### end nested class
|
|
|
|
def sample(self, trans=None):
|
|
"""Adaptive-sampling algorithm that chooses the best sample points
|
|
for a parametric curve between two endpoints and detects
|
|
discontinuities. Called by SVG()."""
|
|
oldrecursionlimit = sys.getrecursionlimit()
|
|
sys.setrecursionlimit(self.recursion_limit + 100)
|
|
try:
|
|
# the best way to keep all the information while sampling is to make a linked list
|
|
if not (self.low < self.high):
|
|
raise ValueError("low must be less than high")
|
|
low, high = self.Sample(float(self.low)), self.Sample(float(self.high))
|
|
low.link(None, high)
|
|
high.link(low, None)
|
|
|
|
low.evaluate(self.f, trans)
|
|
high.evaluate(self.f, trans)
|
|
|
|
# adaptive sampling between the low and high points
|
|
self.subsample(low, high, 0, trans)
|
|
|
|
# Prune excess points where the curve is nearly linear
|
|
left = low
|
|
while left.right is not None:
|
|
# increment mid and right
|
|
mid = left.right
|
|
right = mid.right
|
|
if (right is not None and
|
|
left.X is not None and left.Y is not None and
|
|
mid.X is not None and mid.Y is not None and
|
|
right.X is not None and right.Y is not None):
|
|
numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
|
|
denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
|
|
if denom != 0. and abs(numer/denom) < self.linearity_limit:
|
|
# drop mid (the garbage collector will get it)
|
|
left.right = right
|
|
right.left = left
|
|
else:
|
|
# increment left
|
|
left = left.right
|
|
else:
|
|
left = left.right
|
|
|
|
self.last_samples = self.Samples(low, high)
|
|
|
|
finally:
|
|
sys.setrecursionlimit(oldrecursionlimit)
|
|
|
|
def subsample(self, left, right, depth, trans=None):
|
|
"""Part of the adaptive-sampling algorithm that chooses the best
|
|
sample points. Called by sample()."""
|
|
|
|
if self.random_sampling:
|
|
mid = self.Sample(left.t + random.uniform(0.3, 0.7) * (right.t - left.t))
|
|
else:
|
|
mid = self.Sample(left.t + 0.5 * (right.t - left.t))
|
|
|
|
left.right = mid
|
|
right.left = mid
|
|
mid.link(left, right)
|
|
mid.evaluate(self.f, trans)
|
|
|
|
# calculate the distance of closest approach of mid to the line between left and right
|
|
numer = left.X*(right.Y - mid.Y) + mid.X*(left.Y - right.Y) + right.X*(mid.Y - left.Y)
|
|
denom = math.sqrt((left.X - right.X)**2 + (left.Y - right.Y)**2)
|
|
|
|
# if we haven't sampled enough or left fails to be close enough to right, or mid fails to be linear enough...
|
|
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)):
|
|
|
|
# and we haven't sampled too many points
|
|
if depth < self.recursion_limit:
|
|
self.subsample(left, mid, depth+1, trans)
|
|
self.subsample(mid, right, depth+1, trans)
|
|
|
|
else:
|
|
# We've sampled many points and yet it's still not a small linear gap.
|
|
# Break the line: it's a discontinuity
|
|
mid.y = mid.Y = None
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
return self.Path(trans).SVG()
|
|
|
|
def Path(self, trans=None, local=False):
|
|
"""Apply the transformation "trans" and return a Path object in
|
|
global coordinates. If local=True, return a Path in local coordinates
|
|
(which must be transformed again)."""
|
|
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans)
|
|
if isinstance(self.f, basestring):
|
|
self.f = funcRtoR2(self.f)
|
|
|
|
self.sample(trans)
|
|
|
|
output = []
|
|
for s in self.last_samples:
|
|
if s.X is not None and s.Y is not None:
|
|
if s.left is None or s.left.Y is None:
|
|
command = "M"
|
|
else:
|
|
command = "L"
|
|
|
|
if local:
|
|
output.append((command, s.x, s.y, False))
|
|
else:
|
|
output.append((command, s.X, s.Y, True))
|
|
|
|
if self.loop:
|
|
output.append(("Z",))
|
|
return Path(output, **self.attr)
|
|
|
|
######################################################################
|
|
|
|
class Poly:
|
|
"""Draws a curve specified by a sequence of points. The curve may be
|
|
piecewise linear, like a polygon, or a Bezier curve.
|
|
|
|
Poly(d, mode, loop, attribute=value)
|
|
|
|
d required list of tuples representing points
|
|
and possibly control points
|
|
mode default="L" "lines", "bezier", "velocity",
|
|
"foreback", "smooth", or an abbreviation
|
|
loop default=False if True, connect the first and last
|
|
point, closing the loop
|
|
attribute=value pairs keyword list SVG attributes
|
|
|
|
The format of the tuples in d depends on the mode.
|
|
|
|
"lines"/"L" d=[(x,y), (x,y), ...]
|
|
piecewise-linear segments joining the (x,y) points
|
|
"bezier"/"B" d=[(x, y, c1x, c1y, c2x, c2y), ...]
|
|
Bezier curve with two control points (control points
|
|
precede (x,y), as in SVG paths). If (c1x,c1y) and
|
|
(c2x,c2y) both equal (x,y), you get a linear
|
|
interpolation ("lines")
|
|
"velocity"/"V" d=[(x, y, vx, vy), ...]
|
|
curve that passes through (x,y) with velocity (vx,vy)
|
|
(one unit of arclength per unit time); in other words,
|
|
(vx,vy) is the tangent vector at (x,y). If (vx,vy) is
|
|
(0,0), you get a linear interpolation ("lines").
|
|
"foreback"/"F" d=[(x, y, bx, by, fx, fy), ...]
|
|
like "velocity" except that there is a left derivative
|
|
(bx,by) and a right derivative (fx,fy). If (bx,by)
|
|
equals (fx,fy) (with no minus sign), you get a
|
|
"velocity" curve
|
|
"smooth"/"S" d=[(x,y), (x,y), ...]
|
|
a "velocity" interpolation with (vx,vy)[i] equal to
|
|
((x,y)[i+1] - (x,y)[i-1])/2: the minimal derivative
|
|
"""
|
|
defaults = {}
|
|
|
|
def __repr__(self):
|
|
return "<Poly (%d nodes) mode=%s loop=%s %s>" % (
|
|
len(self.d), self.mode, repr(self.loop), self.attr)
|
|
|
|
def __init__(self, d=[], mode="L", loop=False, **attr):
|
|
self.d = list(d)
|
|
self.mode = mode
|
|
self.loop = loop
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
return self.Path(trans).SVG()
|
|
|
|
def Path(self, trans=None, local=False):
|
|
"""Apply the transformation "trans" and return a Path object in
|
|
global coordinates. If local=True, return a Path in local coordinates
|
|
(which must be transformed again)."""
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans)
|
|
|
|
if self.mode[0] == "L" or self.mode[0] == "l":
|
|
mode = "L"
|
|
elif self.mode[0] == "B" or self.mode[0] == "b":
|
|
mode = "B"
|
|
elif self.mode[0] == "V" or self.mode[0] == "v":
|
|
mode = "V"
|
|
elif self.mode[0] == "F" or self.mode[0] == "f":
|
|
mode = "F"
|
|
elif self.mode[0] == "S" or self.mode[0] == "s":
|
|
mode = "S"
|
|
|
|
vx, vy = [0.]*len(self.d), [0.]*len(self.d)
|
|
for i in xrange(len(self.d)):
|
|
inext = (i+1) % len(self.d)
|
|
iprev = (i-1) % len(self.d)
|
|
|
|
vx[i] = (self.d[inext][0] - self.d[iprev][0])/2.
|
|
vy[i] = (self.d[inext][1] - self.d[iprev][1])/2.
|
|
if not self.loop and (i == 0 or i == len(self.d)-1):
|
|
vx[i], vy[i] = 0., 0.
|
|
|
|
else:
|
|
raise ValueError("mode must be \"lines\", \"bezier\", \"velocity\", \"foreback\", \"smooth\", or an abbreviation")
|
|
|
|
d = []
|
|
indexes = list(range(len(self.d)))
|
|
if self.loop and len(self.d) > 0:
|
|
indexes.append(0)
|
|
|
|
for i in indexes:
|
|
inext = (i+1) % len(self.d)
|
|
iprev = (i-1) % len(self.d)
|
|
|
|
x, y = self.d[i][0], self.d[i][1]
|
|
|
|
if trans is None:
|
|
X, Y = x, y
|
|
else:
|
|
X, Y = trans(x, y)
|
|
|
|
if d == []:
|
|
if local:
|
|
d.append(("M", x, y, False))
|
|
else:
|
|
d.append(("M", X, Y, True))
|
|
|
|
elif mode == "L":
|
|
if local:
|
|
d.append(("L", x, y, False))
|
|
else:
|
|
d.append(("L", X, Y, True))
|
|
|
|
elif mode == "B":
|
|
c1x, c1y = self.d[i][2], self.d[i][3]
|
|
if trans is None:
|
|
C1X, C1Y = c1x, c1y
|
|
else:
|
|
C1X, C1Y = trans(c1x, c1y)
|
|
|
|
c2x, c2y = self.d[i][4], self.d[i][5]
|
|
if trans is None:
|
|
C2X, C2Y = c2x, c2y
|
|
else:
|
|
C2X, C2Y = trans(c2x, c2y)
|
|
|
|
if local:
|
|
d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
|
|
else:
|
|
d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
|
|
|
|
elif mode == "V":
|
|
c1x, c1y = self.d[iprev][2]/3. + self.d[iprev][0], self.d[iprev][3]/3. + self.d[iprev][1]
|
|
c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
|
|
|
|
if trans is None:
|
|
C1X, C1Y = c1x, c1y
|
|
else:
|
|
C1X, C1Y = trans(c1x, c1y)
|
|
if trans is None:
|
|
C2X, C2Y = c2x, c2y
|
|
else:
|
|
C2X, C2Y = trans(c2x, c2y)
|
|
|
|
if local:
|
|
d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
|
|
else:
|
|
d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
|
|
|
|
elif mode == "F":
|
|
c1x, c1y = self.d[iprev][4]/3. + self.d[iprev][0], self.d[iprev][5]/3. + self.d[iprev][1]
|
|
c2x, c2y = self.d[i][2]/-3. + x, self.d[i][3]/-3. + y
|
|
|
|
if trans is None:
|
|
C1X, C1Y = c1x, c1y
|
|
else:
|
|
C1X, C1Y = trans(c1x, c1y)
|
|
if trans is None:
|
|
C2X, C2Y = c2x, c2y
|
|
else:
|
|
C2X, C2Y = trans(c2x, c2y)
|
|
|
|
if local:
|
|
d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
|
|
else:
|
|
d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
|
|
|
|
elif mode == "S":
|
|
c1x, c1y = vx[iprev]/3. + self.d[iprev][0], vy[iprev]/3. + self.d[iprev][1]
|
|
c2x, c2y = vx[i]/-3. + x, vy[i]/-3. + y
|
|
|
|
if trans is None:
|
|
C1X, C1Y = c1x, c1y
|
|
else:
|
|
C1X, C1Y = trans(c1x, c1y)
|
|
if trans is None:
|
|
C2X, C2Y = c2x, c2y
|
|
else:
|
|
C2X, C2Y = trans(c2x, c2y)
|
|
|
|
if local:
|
|
d.append(("C", c1x, c1y, False, c2x, c2y, False, x, y, False))
|
|
else:
|
|
d.append(("C", C1X, C1Y, True, C2X, C2Y, True, X, Y, True))
|
|
|
|
if self.loop and len(self.d) > 0:
|
|
d.append(("Z",))
|
|
|
|
return Path(d, **self.attr)
|
|
|
|
######################################################################
|
|
|
|
class Text:
|
|
"""Draws a text string at a specified point in local coordinates.
|
|
|
|
x, y required location of the point in local coordinates
|
|
d required text/Unicode string
|
|
attribute=value pairs keyword list SVG attributes
|
|
"""
|
|
|
|
defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
|
|
|
|
def __repr__(self):
|
|
return "<Text %s at (%g, %g) %s>" % (repr(self.d), self.x, self.y, self.attr)
|
|
|
|
def __init__(self, x, y, d, **attr):
|
|
self.x = x
|
|
self.y = y
|
|
self.d = unicode(d)
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans)
|
|
|
|
X, Y = self.x, self.y
|
|
if trans is not None:
|
|
X, Y = trans(X, Y)
|
|
return SVG("text", self.d, x=X, y=Y, **self.attr)
|
|
|
|
|
|
class TextGlobal:
|
|
"""Draws a text string at a specified point in global coordinates.
|
|
|
|
x, y required location of the point in global coordinates
|
|
d required text/Unicode string
|
|
attribute=value pairs keyword list SVG attributes
|
|
"""
|
|
defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
|
|
|
|
def __repr__(self):
|
|
return "<TextGlobal %s at (%s, %s) %s>" % (repr(self.d), str(self.x), str(self.y), self.attr)
|
|
|
|
def __init__(self, x, y, d, **attr):
|
|
self.x = x
|
|
self.y = y
|
|
self.d = unicode(d)
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
return SVG("text", self.d, x=self.x, y=self.y, **self.attr)
|
|
|
|
######################################################################
|
|
|
|
_symbol_templates = {"dot": SVG("symbol", SVG("circle", cx=0, cy=0, r=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"),
|
|
"box": SVG("symbol", SVG("rect", x1=-1, y1=-1, x2=1, y2=1, stroke="none", fill="black"), viewBox="0 0 1 1", overflow="visible"),
|
|
"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"),
|
|
"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"),
|
|
}
|
|
|
|
def make_symbol(id, shape="dot", **attr):
|
|
"""Creates a new instance of an SVG symbol to avoid cross-linking objects.
|
|
|
|
id required a new identifier (string/Unicode)
|
|
shape default="dot" the shape name from _symbol_templates
|
|
attribute=value list keyword list modify the SVG attributes of the new symbol
|
|
"""
|
|
output = copy.deepcopy(_symbol_templates[shape])
|
|
for i in output.sub:
|
|
i.attr.update(attr_preprocess(attr))
|
|
output["id"] = id
|
|
return output
|
|
|
|
_circular_dot = make_symbol("circular_dot")
|
|
|
|
|
|
class Dots:
|
|
"""Dots draws SVG symbols at a set of points.
|
|
|
|
d required list of (x,y) points
|
|
symbol default=None SVG symbol or a new identifier to
|
|
label an auto-generated symbol;
|
|
if None, use pre-defined _circular_dot
|
|
width, height default=1, 1 width and height of the symbols
|
|
in SVG coordinates
|
|
attribute=value pairs keyword list SVG attributes
|
|
"""
|
|
defaults = {}
|
|
|
|
def __repr__(self):
|
|
return "<Dots (%d nodes) %s>" % (len(self.d), self.attr)
|
|
|
|
def __init__(self, d=[], symbol=None, width=1., height=1., **attr):
|
|
self.d = list(d)
|
|
self.width = width
|
|
self.height = height
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
if symbol is None:
|
|
self.symbol = _circular_dot
|
|
elif isinstance(symbol, SVG):
|
|
self.symbol = symbol
|
|
else:
|
|
self.symbol = make_symbol(symbol)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans)
|
|
|
|
output = SVG("g", SVG("defs", self.symbol))
|
|
id = "#%s" % self.symbol["id"]
|
|
|
|
for p in self.d:
|
|
x, y = p[0], p[1]
|
|
|
|
if trans is None:
|
|
X, Y = x, y
|
|
else:
|
|
X, Y = trans(x, y)
|
|
|
|
item = SVG("use", x=X, y=Y, xlink__href=id)
|
|
if self.width is not None:
|
|
item["width"] = self.width
|
|
if self.height is not None:
|
|
item["height"] = self.height
|
|
output.append(item)
|
|
|
|
return output
|
|
|
|
######################################################################
|
|
|
|
_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"),
|
|
"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"),
|
|
}
|
|
|
|
def make_marker(id, shape, **attr):
|
|
"""Creates a new instance of an SVG marker to avoid cross-linking objects.
|
|
|
|
id required a new identifier (string/Unicode)
|
|
shape required the shape name from _marker_templates
|
|
attribute=value list keyword list modify the SVG attributes of the new marker
|
|
"""
|
|
output = copy.deepcopy(_marker_templates[shape])
|
|
for i in output.sub:
|
|
i.attr.update(attr_preprocess(attr))
|
|
output["id"] = id
|
|
return output
|
|
|
|
|
|
class Line(Curve):
|
|
"""Draws a line between two points.
|
|
|
|
Line(x1, y1, x2, y2, arrow_start, arrow_end, attribute=value)
|
|
|
|
x1, y1 required the starting point
|
|
x2, y2 required the ending point
|
|
arrow_start default=None if an identifier string/Unicode,
|
|
draw a new arrow object at the
|
|
beginning of the line; if a marker,
|
|
draw that marker instead
|
|
arrow_end default=None same for the end of the line
|
|
attribute=value pairs keyword list SVG attributes
|
|
"""
|
|
defaults = {}
|
|
|
|
def __repr__(self):
|
|
return "<Line (%g, %g) to (%g, %g) %s>" % (
|
|
self.x1, self.y1, self.x2, self.y2, self.attr)
|
|
|
|
def __init__(self, x1, y1, x2, y2, arrow_start=None, arrow_end=None, **attr):
|
|
self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
|
|
self.arrow_start, self.arrow_end = arrow_start, arrow_end
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
|
|
line = self.Path(trans).SVG()
|
|
|
|
if ((self.arrow_start != False and self.arrow_start is not None) or
|
|
(self.arrow_end != False and self.arrow_end is not None)):
|
|
defs = SVG("defs")
|
|
|
|
if self.arrow_start != False and self.arrow_start is not None:
|
|
if isinstance(self.arrow_start, SVG):
|
|
defs.append(self.arrow_start)
|
|
line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
|
|
elif isinstance(self.arrow_start, basestring):
|
|
defs.append(make_marker(self.arrow_start, "arrow_start"))
|
|
line.attr["marker-start"] = "url(#%s)" % self.arrow_start
|
|
else:
|
|
raise TypeError("arrow_start must be False/None or an id string for the new marker")
|
|
|
|
if self.arrow_end != False and self.arrow_end is not None:
|
|
if isinstance(self.arrow_end, SVG):
|
|
defs.append(self.arrow_end)
|
|
line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
|
|
elif isinstance(self.arrow_end, basestring):
|
|
defs.append(make_marker(self.arrow_end, "arrow_end"))
|
|
line.attr["marker-end"] = "url(#%s)" % self.arrow_end
|
|
else:
|
|
raise TypeError("arrow_end must be False/None or an id string for the new marker")
|
|
|
|
return SVG("g", defs, line)
|
|
|
|
return line
|
|
|
|
def Path(self, trans=None, local=False):
|
|
"""Apply the transformation "trans" and return a Path object in
|
|
global coordinates. If local=True, return a Path in local coordinates
|
|
(which must be transformed again)."""
|
|
self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1 + t*(self.y2 - self.y1))
|
|
self.low = 0.
|
|
self.high = 1.
|
|
self.loop = False
|
|
|
|
if trans is None:
|
|
return Path([("M", self.x1, self.y1, not local), ("L", self.x2, self.y2, not local)], **self.attr)
|
|
else:
|
|
return Curve.Path(self, trans, local)
|
|
|
|
|
|
class LineGlobal:
|
|
"""Draws a line between two points, one or both of which is in
|
|
global coordinates.
|
|
|
|
Line(x1, y1, x2, y2, lcoal1, local2, arrow_start, arrow_end, attribute=value)
|
|
|
|
x1, y1 required the starting point
|
|
x2, y2 required the ending point
|
|
local1 default=False if True, interpret first point as a
|
|
local coordinate (apply transform)
|
|
local2 default=False if True, interpret second point as a
|
|
local coordinate (apply transform)
|
|
arrow_start default=None if an identifier string/Unicode,
|
|
draw a new arrow object at the
|
|
beginning of the line; if a marker,
|
|
draw that marker instead
|
|
arrow_end default=None same for the end of the line
|
|
attribute=value pairs keyword list SVG attributes
|
|
"""
|
|
defaults = {}
|
|
|
|
def __repr__(self):
|
|
local1, local2 = "", ""
|
|
if self.local1:
|
|
local1 = "L"
|
|
if self.local2:
|
|
local2 = "L"
|
|
|
|
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)
|
|
|
|
def __init__(self, x1, y1, x2, y2, local1=False, local2=False, arrow_start=None, arrow_end=None, **attr):
|
|
self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
|
|
self.local1, self.local2 = local1, local2
|
|
self.arrow_start, self.arrow_end = arrow_start, arrow_end
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans)
|
|
|
|
X1, Y1, X2, Y2 = self.x1, self.y1, self.x2, self.y2
|
|
|
|
if self.local1:
|
|
X1, Y1 = trans(X1, Y1)
|
|
if self.local2:
|
|
X2, Y2 = trans(X2, Y2)
|
|
|
|
line = SVG("path", d="M%s %s L%s %s" % (X1, Y1, X2, Y2), **self.attr)
|
|
|
|
if ((self.arrow_start != False and self.arrow_start is not None) or
|
|
(self.arrow_end != False and self.arrow_end is not None)):
|
|
defs = SVG("defs")
|
|
|
|
if self.arrow_start != False and self.arrow_start is not None:
|
|
if isinstance(self.arrow_start, SVG):
|
|
defs.append(self.arrow_start)
|
|
line.attr["marker-start"] = "url(#%s)" % self.arrow_start["id"]
|
|
elif isinstance(self.arrow_start, basestring):
|
|
defs.append(make_marker(self.arrow_start, "arrow_start"))
|
|
line.attr["marker-start"] = "url(#%s)" % self.arrow_start
|
|
else:
|
|
raise TypeError("arrow_start must be False/None or an id string for the new marker")
|
|
|
|
if self.arrow_end != False and self.arrow_end is not None:
|
|
if isinstance(self.arrow_end, SVG):
|
|
defs.append(self.arrow_end)
|
|
line.attr["marker-end"] = "url(#%s)" % self.arrow_end["id"]
|
|
elif isinstance(self.arrow_end, basestring):
|
|
defs.append(make_marker(self.arrow_end, "arrow_end"))
|
|
line.attr["marker-end"] = "url(#%s)" % self.arrow_end
|
|
else:
|
|
raise TypeError("arrow_end must be False/None or an id string for the new marker")
|
|
|
|
return SVG("g", defs, line)
|
|
|
|
return line
|
|
|
|
|
|
class VLine(Line):
|
|
"""Draws a vertical line.
|
|
|
|
VLine(y1, y2, x, attribute=value)
|
|
|
|
y1, y2 required y range
|
|
x required x position
|
|
attribute=value pairs keyword list SVG attributes
|
|
"""
|
|
defaults = {}
|
|
|
|
def __repr__(self):
|
|
return "<VLine (%g, %g) at x=%s %s>" % (self.y1, self.y2, self.x, self.attr)
|
|
|
|
def __init__(self, y1, y2, x, **attr):
|
|
self.x = x
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
Line.__init__(self, x, y1, x, y2, **self.attr)
|
|
|
|
def Path(self, trans=None, local=False):
|
|
"""Apply the transformation "trans" and return a Path object in
|
|
global coordinates. If local=True, return a Path in local coordinates
|
|
(which must be transformed again)."""
|
|
self.x1 = self.x
|
|
self.x2 = self.x
|
|
return Line.Path(self, trans, local)
|
|
|
|
|
|
class HLine(Line):
|
|
"""Draws a horizontal line.
|
|
|
|
HLine(x1, x2, y, attribute=value)
|
|
|
|
x1, x2 required x range
|
|
y required y position
|
|
attribute=value pairs keyword list SVG attributes
|
|
"""
|
|
defaults = {}
|
|
|
|
def __repr__(self):
|
|
return "<HLine (%g, %g) at y=%s %s>" % (self.x1, self.x2, self.y, self.attr)
|
|
|
|
def __init__(self, x1, x2, y, **attr):
|
|
self.y = y
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
Line.__init__(self, x1, y, x2, y, **self.attr)
|
|
|
|
def Path(self, trans=None, local=False):
|
|
"""Apply the transformation "trans" and return a Path object in
|
|
global coordinates. If local=True, return a Path in local coordinates
|
|
(which must be transformed again)."""
|
|
self.y1 = self.y
|
|
self.y2 = self.y
|
|
return Line.Path(self, trans, local)
|
|
|
|
######################################################################
|
|
|
|
class Rect(Curve):
|
|
"""Draws a rectangle.
|
|
|
|
Rect(x1, y1, x2, y2, attribute=value)
|
|
|
|
x1, y1 required the starting point
|
|
x2, y2 required the ending point
|
|
attribute=value pairs keyword list SVG attributes
|
|
"""
|
|
defaults = {}
|
|
|
|
def __repr__(self):
|
|
return "<Rect (%g, %g), (%g, %g) %s>" % (
|
|
self.x1, self.y1, self.x2, self.y2, self.attr)
|
|
|
|
def __init__(self, x1, y1, x2, y2, **attr):
|
|
self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
return self.Path(trans).SVG()
|
|
|
|
def Path(self, trans=None, local=False):
|
|
"""Apply the transformation "trans" and return a Path object in
|
|
global coordinates. If local=True, return a Path in local coordinates
|
|
(which must be transformed again)."""
|
|
if trans is None:
|
|
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)
|
|
|
|
else:
|
|
self.low = 0.
|
|
self.high = 1.
|
|
self.loop = False
|
|
|
|
self.f = lambda t: (self.x1 + t*(self.x2 - self.x1), self.y1)
|
|
d1 = Curve.Path(self, trans, local).d
|
|
|
|
self.f = lambda t: (self.x2, self.y1 + t*(self.y2 - self.y1))
|
|
d2 = Curve.Path(self, trans, local).d
|
|
del d2[0]
|
|
|
|
self.f = lambda t: (self.x2 + t*(self.x1 - self.x2), self.y2)
|
|
d3 = Curve.Path(self, trans, local).d
|
|
del d3[0]
|
|
|
|
self.f = lambda t: (self.x1, self.y2 + t*(self.y1 - self.y2))
|
|
d4 = Curve.Path(self, trans, local).d
|
|
del d4[0]
|
|
|
|
return Path(d=(d1 + d2 + d3 + d4 + [("Z",)]), **self.attr)
|
|
|
|
######################################################################
|
|
|
|
class Ellipse(Curve):
|
|
"""Draws an ellipse from a semimajor vector (ax,ay) and a semiminor
|
|
length (b).
|
|
|
|
Ellipse(x, y, ax, ay, b, attribute=value)
|
|
|
|
x, y required the center of the ellipse/circle
|
|
ax, ay required a vector indicating the length
|
|
and direction of the semimajor axis
|
|
b required the length of the semiminor axis.
|
|
If equal to sqrt(ax2 + ay2), the
|
|
ellipse is a circle
|
|
attribute=value pairs keyword list SVG attributes
|
|
|
|
(If sqrt(ax**2 + ay**2) is less than b, then (ax,ay) is actually the
|
|
semiminor axis.)
|
|
"""
|
|
defaults = {}
|
|
|
|
def __repr__(self):
|
|
return "<Ellipse (%g, %g) a=(%g, %g), b=%g %s>" % (
|
|
self.x, self.y, self.ax, self.ay, self.b, self.attr)
|
|
|
|
def __init__(self, x, y, ax, ay, b, **attr):
|
|
self.x, self.y, self.ax, self.ay, self.b = x, y, ax, ay, b
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
return self.Path(trans).SVG()
|
|
|
|
def Path(self, trans=None, local=False):
|
|
"""Apply the transformation "trans" and return a Path object in
|
|
global coordinates. If local=True, return a Path in local coordinates
|
|
(which must be transformed again)."""
|
|
angle = math.atan2(self.ay, self.ax) + math.pi/2.
|
|
bx = self.b * math.cos(angle)
|
|
by = self.b * math.sin(angle)
|
|
|
|
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))
|
|
self.low = -math.pi
|
|
self.high = math.pi
|
|
self.loop = True
|
|
return Curve.Path(self, trans, local)
|
|
|
|
######################################################################
|
|
|
|
def unumber(x):
|
|
"""Converts numbers to a Unicode string, taking advantage of special
|
|
Unicode characters to make nice minus signs and scientific notation.
|
|
"""
|
|
output = u"%g" % x
|
|
|
|
if output[0] == u"-":
|
|
output = u"\u2013" + output[1:]
|
|
|
|
index = output.find(u"e")
|
|
if index != -1:
|
|
uniout = unicode(output[:index]) + u"\u00d710"
|
|
saw_nonzero = False
|
|
for n in output[index+1:]:
|
|
if n == u"+":
|
|
pass # uniout += u"\u207a"
|
|
elif n == u"-":
|
|
uniout += u"\u207b"
|
|
elif n == u"0":
|
|
if saw_nonzero:
|
|
uniout += u"\u2070"
|
|
elif n == u"1":
|
|
saw_nonzero = True
|
|
uniout += u"\u00b9"
|
|
elif n == u"2":
|
|
saw_nonzero = True
|
|
uniout += u"\u00b2"
|
|
elif n == u"3":
|
|
saw_nonzero = True
|
|
uniout += u"\u00b3"
|
|
elif u"4" <= n <= u"9":
|
|
saw_nonzero = True
|
|
if saw_nonzero:
|
|
uniout += eval("u\"\\u%x\"" % (0x2070 + ord(n) - ord(u"0")))
|
|
else:
|
|
uniout += n
|
|
|
|
if uniout[:2] == u"1\u00d7":
|
|
uniout = uniout[2:]
|
|
return uniout
|
|
|
|
return output
|
|
|
|
|
|
class Ticks:
|
|
"""Superclass for all graphics primitives that draw ticks,
|
|
miniticks, and tick labels. This class only draws the ticks.
|
|
|
|
Ticks(f, low, high, ticks, miniticks, labels, logbase, arrow_start,
|
|
arrow_end, text_attr, attribute=value)
|
|
|
|
f required parametric function along which ticks
|
|
will be drawn; has the same format as
|
|
the function used in Curve
|
|
low, high required range of the independent variable
|
|
ticks default=-10 request ticks according to the standard
|
|
tick specification (see below)
|
|
miniticks default=True request miniticks according to the
|
|
standard minitick specification (below)
|
|
labels True request tick labels according to the
|
|
standard tick label specification (below)
|
|
logbase default=None if a number, the axis is logarithmic with
|
|
ticks at the given base (usually 10)
|
|
arrow_start default=None if a new string identifier, draw an arrow
|
|
at the low-end of the axis, referenced by
|
|
that identifier; if an SVG marker object,
|
|
use that marker
|
|
arrow_end default=None if a new string identifier, draw an arrow
|
|
at the high-end of the axis, referenced by
|
|
that identifier; if an SVG marker object,
|
|
use that marker
|
|
text_attr default={} SVG attributes for the text labels
|
|
attribute=value pairs keyword list SVG attributes for the tick marks
|
|
|
|
Standard tick specification:
|
|
|
|
* True: same as -10 (below).
|
|
* Positive number N: draw exactly N ticks, including the endpoints. To
|
|
subdivide an axis into 10 equal-sized segments, ask for 11 ticks.
|
|
* Negative number -N: draw at least N ticks. Ticks will be chosen with
|
|
"natural" values, multiples of 2 or 5.
|
|
* List of values: draw a tick mark at each value.
|
|
* Dict of value, label pairs: draw a tick mark at each value, labeling
|
|
it with the given string. This lets you say things like {3.14159: "pi"}.
|
|
* False or None: no ticks.
|
|
|
|
Standard minitick specification:
|
|
|
|
* True: draw miniticks with "natural" values, more closely spaced than
|
|
the ticks.
|
|
* Positive number N: draw exactly N miniticks, including the endpoints.
|
|
To subdivide an axis into 100 equal-sized segments, ask for 101 miniticks.
|
|
* Negative number -N: draw at least N miniticks.
|
|
* List of values: draw a minitick mark at each value.
|
|
* False or None: no miniticks.
|
|
|
|
Standard tick label specification:
|
|
|
|
* True: use the unumber function (described below)
|
|
* Format string: standard format strings, e.g. "%5.2f" for 12.34
|
|
* Python callable: function that converts numbers to strings
|
|
* False or None: no labels
|
|
"""
|
|
defaults = {"stroke-width": "0.25pt", }
|
|
text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
|
|
tick_start = -1.5
|
|
tick_end = 1.5
|
|
minitick_start = -0.75
|
|
minitick_end = 0.75
|
|
text_start = 2.5
|
|
text_angle = 0.
|
|
|
|
def __repr__(self):
|
|
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)
|
|
|
|
def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None,
|
|
arrow_start=None, arrow_end=None, text_attr={}, **attr):
|
|
self.f = f
|
|
self.low = low
|
|
self.high = high
|
|
self.ticks = ticks
|
|
self.miniticks = miniticks
|
|
self.labels = labels
|
|
self.logbase = logbase
|
|
self.arrow_start = arrow_start
|
|
self.arrow_end = arrow_end
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
self.text_attr = dict(self.text_defaults)
|
|
self.text_attr.update(text_attr)
|
|
|
|
def orient_tickmark(self, t, trans=None):
|
|
"""Return the position, normalized local x vector, normalized
|
|
local y vector, and angle of a tick at position t.
|
|
|
|
Normally only used internally.
|
|
"""
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans)
|
|
if trans is None:
|
|
f = self.f
|
|
else:
|
|
f = lambda t: trans(*self.f(t))
|
|
|
|
eps = _epsilon * abs(self.high - self.low)
|
|
|
|
X, Y = f(t)
|
|
Xprime, Yprime = f(t + eps)
|
|
xhatx, xhaty = (Xprime - X)/eps, (Yprime - Y)/eps
|
|
|
|
norm = math.sqrt(xhatx**2 + xhaty**2)
|
|
if norm != 0:
|
|
xhatx, xhaty = xhatx/norm, xhaty/norm
|
|
else:
|
|
xhatx, xhaty = 1., 0.
|
|
|
|
angle = math.atan2(xhaty, xhatx) + math.pi/2.
|
|
yhatx, yhaty = math.cos(angle), math.sin(angle)
|
|
|
|
return (X, Y), (xhatx, xhaty), (yhatx, yhaty), angle
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans)
|
|
|
|
self.last_ticks, self.last_miniticks = self.interpret()
|
|
tickmarks = Path([], **self.attr)
|
|
minitickmarks = Path([], **self.attr)
|
|
output = SVG("g")
|
|
|
|
if ((self.arrow_start != False and self.arrow_start is not None) or
|
|
(self.arrow_end != False and self.arrow_end is not None)):
|
|
defs = SVG("defs")
|
|
|
|
if self.arrow_start != False and self.arrow_start is not None:
|
|
if isinstance(self.arrow_start, SVG):
|
|
defs.append(self.arrow_start)
|
|
elif isinstance(self.arrow_start, basestring):
|
|
defs.append(make_marker(self.arrow_start, "arrow_start"))
|
|
else:
|
|
raise TypeError("arrow_start must be False/None or an id string for the new marker")
|
|
|
|
if self.arrow_end != False and self.arrow_end is not None:
|
|
if isinstance(self.arrow_end, SVG):
|
|
defs.append(self.arrow_end)
|
|
elif isinstance(self.arrow_end, basestring):
|
|
defs.append(make_marker(self.arrow_end, "arrow_end"))
|
|
else:
|
|
raise TypeError("arrow_end must be False/None or an id string for the new marker")
|
|
|
|
output.append(defs)
|
|
|
|
eps = _epsilon * (self.high - self.low)
|
|
|
|
for t, label in self.last_ticks.items():
|
|
(X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
|
|
|
|
if ((not self.arrow_start or abs(t - self.low) > eps) and
|
|
(not self.arrow_end or abs(t - self.high) > eps)):
|
|
tickmarks.d.append(("M", X - yhatx*self.tick_start, Y - yhaty*self.tick_start, True))
|
|
tickmarks.d.append(("L", X - yhatx*self.tick_end, Y - yhaty*self.tick_end, True))
|
|
|
|
angle = (angle - math.pi/2.)*180./math.pi + self.text_angle
|
|
|
|
########### a HACK! ############ (to be removed when Inkscape handles baselines)
|
|
if _hacks["inkscape-text-vertical-shift"]:
|
|
if self.text_start > 0:
|
|
X += math.cos(angle*math.pi/180. + math.pi/2.) * 2.
|
|
Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2.
|
|
else:
|
|
X += math.cos(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
|
|
Y += math.sin(angle*math.pi/180. + math.pi/2.) * 2. * 2.5
|
|
########### end hack ###########
|
|
|
|
if label != "":
|
|
output.append(SVG("text", label, transform="translate(%g, %g) rotate(%g)" %
|
|
(X - yhatx*self.text_start, Y - yhaty*self.text_start, angle), **self.text_attr))
|
|
|
|
for t in self.last_miniticks:
|
|
skip = False
|
|
for tt in self.last_ticks.keys():
|
|
if abs(t - tt) < eps:
|
|
skip = True
|
|
break
|
|
if not skip:
|
|
(X, Y), (xhatx, xhaty), (yhatx, yhaty), angle = self.orient_tickmark(t, trans)
|
|
|
|
if ((not self.arrow_start or abs(t - self.low) > eps) and
|
|
(not self.arrow_end or abs(t - self.high) > eps)):
|
|
minitickmarks.d.append(("M", X - yhatx*self.minitick_start, Y - yhaty*self.minitick_start, True))
|
|
minitickmarks.d.append(("L", X - yhatx*self.minitick_end, Y - yhaty*self.minitick_end, True))
|
|
|
|
output.prepend(tickmarks.SVG(trans))
|
|
output.prepend(minitickmarks.SVG(trans))
|
|
return output
|
|
|
|
def interpret(self):
|
|
"""Evaluate and return optimal ticks and miniticks according to
|
|
the standard minitick specification.
|
|
|
|
Normally only used internally.
|
|
"""
|
|
|
|
if self.labels is None or self.labels == False:
|
|
format = lambda x: ""
|
|
|
|
elif self.labels == True:
|
|
format = unumber
|
|
|
|
elif isinstance(self.labels, basestring):
|
|
format = lambda x: (self.labels % x)
|
|
|
|
elif callable(self.labels):
|
|
format = self.labels
|
|
|
|
else:
|
|
raise TypeError("labels must be None/False, True, a format string, or a number->string function")
|
|
|
|
# Now for the ticks
|
|
ticks = self.ticks
|
|
|
|
# Case 1: ticks is None/False
|
|
if ticks is None or ticks == False:
|
|
return {}, []
|
|
|
|
# Case 2: ticks is the number of desired ticks
|
|
elif isinstance(ticks, (int, long)):
|
|
if ticks == True:
|
|
ticks = -10
|
|
|
|
if self.logbase is None:
|
|
ticks = self.compute_ticks(ticks, format)
|
|
else:
|
|
ticks = self.compute_logticks(self.logbase, ticks, format)
|
|
|
|
# Now for the miniticks
|
|
if self.miniticks == True:
|
|
if self.logbase is None:
|
|
return ticks, self.compute_miniticks(ticks)
|
|
else:
|
|
return ticks, self.compute_logminiticks(self.logbase)
|
|
|
|
elif isinstance(self.miniticks, (int, long)):
|
|
return ticks, self.regular_miniticks(self.miniticks)
|
|
|
|
elif getattr(self.miniticks, "__iter__", False):
|
|
return ticks, self.miniticks
|
|
|
|
elif self.miniticks == False or self.miniticks is None:
|
|
return ticks, []
|
|
|
|
else:
|
|
raise TypeError("miniticks must be None/False, True, a number of desired miniticks, or a list of numbers")
|
|
|
|
# Cases 3 & 4: ticks is iterable
|
|
elif getattr(ticks, "__iter__", False):
|
|
|
|
# Case 3: ticks is some kind of list
|
|
if not isinstance(ticks, dict):
|
|
output = {}
|
|
eps = _epsilon * (self.high - self.low)
|
|
for x in ticks:
|
|
if format == unumber and abs(x) < eps:
|
|
output[x] = u"0"
|
|
else:
|
|
output[x] = format(x)
|
|
ticks = output
|
|
|
|
# Case 4: ticks is a dict
|
|
else:
|
|
pass
|
|
|
|
# Now for the miniticks
|
|
if self.miniticks == True:
|
|
if self.logbase is None:
|
|
return ticks, self.compute_miniticks(ticks)
|
|
else:
|
|
return ticks, self.compute_logminiticks(self.logbase)
|
|
|
|
elif isinstance(self.miniticks, (int, long)):
|
|
return ticks, self.regular_miniticks(self.miniticks)
|
|
|
|
elif getattr(self.miniticks, "__iter__", False):
|
|
return ticks, self.miniticks
|
|
|
|
elif self.miniticks == False or self.miniticks is None:
|
|
return ticks, []
|
|
|
|
else:
|
|
raise TypeError("miniticks must be None/False, True, a number of desired miniticks, or a list of numbers")
|
|
|
|
else:
|
|
raise TypeError("ticks must be None/False, a number of desired ticks, a list of numbers, or a dictionary of explicit markers")
|
|
|
|
def compute_ticks(self, N, format):
|
|
"""Return less than -N or exactly N optimal linear ticks.
|
|
|
|
Normally only used internally.
|
|
"""
|
|
if self.low >= self.high:
|
|
raise ValueError("low must be less than high")
|
|
if N == 1:
|
|
raise ValueError("N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum")
|
|
|
|
eps = _epsilon * (self.high - self.low)
|
|
|
|
if N >= 0:
|
|
output = {}
|
|
x = self.low
|
|
for i in xrange(N):
|
|
if format == unumber and abs(x) < eps:
|
|
label = u"0"
|
|
else:
|
|
label = format(x)
|
|
output[x] = label
|
|
x += (self.high - self.low)/(N-1.)
|
|
return output
|
|
|
|
N = -N
|
|
|
|
counter = 0
|
|
granularity = 10**math.ceil(math.log10(max(abs(self.low), abs(self.high))))
|
|
lowN = math.ceil(1.*self.low / granularity)
|
|
highN = math.floor(1.*self.high / granularity)
|
|
|
|
while lowN > highN:
|
|
countermod3 = counter % 3
|
|
if countermod3 == 0:
|
|
granularity *= 0.5
|
|
elif countermod3 == 1:
|
|
granularity *= 0.4
|
|
else:
|
|
granularity *= 0.5
|
|
counter += 1
|
|
lowN = math.ceil(1.*self.low / granularity)
|
|
highN = math.floor(1.*self.high / granularity)
|
|
|
|
last_granularity = granularity
|
|
last_trial = None
|
|
|
|
while True:
|
|
trial = {}
|
|
for n in range(int(lowN), int(highN)+1):
|
|
x = n * granularity
|
|
if format == unumber and abs(x) < eps:
|
|
label = u"0"
|
|
else:
|
|
label = format(x)
|
|
trial[x] = label
|
|
|
|
if int(highN)+1 - int(lowN) >= N:
|
|
if last_trial is None:
|
|
v1, v2 = self.low, self.high
|
|
return {v1: format(v1), v2: format(v2)}
|
|
else:
|
|
low_in_ticks, high_in_ticks = False, False
|
|
for t in last_trial.keys():
|
|
if 1.*abs(t - self.low)/last_granularity < _epsilon:
|
|
low_in_ticks = True
|
|
if 1.*abs(t - self.high)/last_granularity < _epsilon:
|
|
high_in_ticks = True
|
|
|
|
lowN = 1.*self.low / last_granularity
|
|
highN = 1.*self.high / last_granularity
|
|
if abs(lowN - round(lowN)) < _epsilon and not low_in_ticks:
|
|
last_trial[self.low] = format(self.low)
|
|
if abs(highN - round(highN)) < _epsilon and not high_in_ticks:
|
|
last_trial[self.high] = format(self.high)
|
|
return last_trial
|
|
|
|
last_granularity = granularity
|
|
last_trial = trial
|
|
|
|
countermod3 = counter % 3
|
|
if countermod3 == 0:
|
|
granularity *= 0.5
|
|
elif countermod3 == 1:
|
|
granularity *= 0.4
|
|
else:
|
|
granularity *= 0.5
|
|
counter += 1
|
|
lowN = math.ceil(1.*self.low / granularity)
|
|
highN = math.floor(1.*self.high / granularity)
|
|
|
|
def regular_miniticks(self, N):
|
|
"""Return exactly N linear ticks.
|
|
|
|
Normally only used internally.
|
|
"""
|
|
output = []
|
|
x = self.low
|
|
for i in xrange(N):
|
|
output.append(x)
|
|
x += (self.high - self.low)/(N-1.)
|
|
return output
|
|
|
|
def compute_miniticks(self, original_ticks):
|
|
"""Return optimal linear miniticks, given a set of ticks.
|
|
|
|
Normally only used internally.
|
|
"""
|
|
if len(original_ticks) < 2:
|
|
original_ticks = ticks(self.low, self.high) # XXX ticks is undefined!
|
|
original_ticks = original_ticks.keys()
|
|
original_ticks.sort()
|
|
|
|
if self.low > original_ticks[0] + _epsilon or self.high < original_ticks[-1] - _epsilon:
|
|
raise ValueError("original_ticks {%g...%g} extend beyond [%g, %g]" % (original_ticks[0], original_ticks[-1], self.low, self.high))
|
|
|
|
granularities = []
|
|
for i in range(len(original_ticks)-1):
|
|
granularities.append(original_ticks[i+1] - original_ticks[i])
|
|
spacing = 10**(math.ceil(math.log10(min(granularities)) - 1))
|
|
|
|
output = []
|
|
x = original_ticks[0] - math.ceil(1.*(original_ticks[0] - self.low) / spacing) * spacing
|
|
|
|
while x <= self.high:
|
|
if x >= self.low:
|
|
already_in_ticks = False
|
|
for t in original_ticks:
|
|
if abs(x-t) < _epsilon * (self.high - self.low):
|
|
already_in_ticks = True
|
|
if not already_in_ticks:
|
|
output.append(x)
|
|
x += spacing
|
|
return output
|
|
|
|
def compute_logticks(self, base, N, format):
|
|
"""Return less than -N or exactly N optimal logarithmic ticks.
|
|
|
|
Normally only used internally.
|
|
"""
|
|
if self.low >= self.high:
|
|
raise ValueError("low must be less than high")
|
|
if N == 1:
|
|
raise ValueError("N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum")
|
|
|
|
eps = _epsilon * (self.high - self.low)
|
|
|
|
if N >= 0:
|
|
output = {}
|
|
x = self.low
|
|
for i in xrange(N):
|
|
if format == unumber and abs(x) < eps:
|
|
label = u"0"
|
|
else:
|
|
label = format(x)
|
|
output[x] = label
|
|
x += (self.high - self.low)/(N-1.)
|
|
return output
|
|
|
|
N = -N
|
|
|
|
lowN = math.floor(math.log(self.low, base))
|
|
highN = math.ceil(math.log(self.high, base))
|
|
output = {}
|
|
for n in range(int(lowN), int(highN)+1):
|
|
x = base**n
|
|
label = format(x)
|
|
if self.low <= x <= self.high:
|
|
output[x] = label
|
|
|
|
for i in range(1, len(output)):
|
|
keys = output.keys()
|
|
keys.sort()
|
|
keys = keys[::i]
|
|
values = map(lambda k: output[k], keys)
|
|
if len(values) <= N:
|
|
for k in output.keys():
|
|
if k not in keys:
|
|
output[k] = ""
|
|
break
|
|
|
|
if len(output) <= 2:
|
|
output2 = self.compute_ticks(N=-int(math.ceil(N/2.)), format=format)
|
|
lowest = min(output2)
|
|
|
|
for k in output:
|
|
if k < lowest:
|
|
output2[k] = output[k]
|
|
output = output2
|
|
|
|
return output
|
|
|
|
def compute_logminiticks(self, base):
|
|
"""Return optimal logarithmic miniticks, given a set of ticks.
|
|
|
|
Normally only used internally.
|
|
"""
|
|
if self.low >= self.high:
|
|
raise ValueError("low must be less than high")
|
|
|
|
lowN = math.floor(math.log(self.low, base))
|
|
highN = math.ceil(math.log(self.high, base))
|
|
output = []
|
|
num_ticks = 0
|
|
for n in range(int(lowN), int(highN)+1):
|
|
x = base**n
|
|
if self.low <= x <= self.high:
|
|
num_ticks += 1
|
|
for m in range(2, int(math.ceil(base))):
|
|
minix = m * x
|
|
if self.low <= minix <= self.high:
|
|
output.append(minix)
|
|
|
|
if num_ticks <= 2:
|
|
return []
|
|
else:
|
|
return output
|
|
|
|
######################################################################
|
|
|
|
class CurveAxis(Curve, Ticks):
|
|
"""Draw an axis with tick marks along a parametric curve.
|
|
|
|
CurveAxis(f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
|
|
text_attr, attribute=value)
|
|
|
|
f required a Python callable or string in
|
|
the form "f(t), g(t)", just like Curve
|
|
low, high required left and right endpoints
|
|
ticks default=-10 request ticks according to the standard
|
|
tick specification (see help(Ticks))
|
|
miniticks default=True request miniticks according to the
|
|
standard minitick specification
|
|
labels True request tick labels according to the
|
|
standard tick label specification
|
|
logbase default=None if a number, the x axis is logarithmic
|
|
with ticks at the given base (10 being
|
|
the most common)
|
|
arrow_start default=None if a new string identifier, draw an
|
|
arrow at the low-end of the axis,
|
|
referenced by that identifier; if an
|
|
SVG marker object, use that marker
|
|
arrow_end default=None if a new string identifier, draw an
|
|
arrow at the high-end of the axis,
|
|
referenced by that identifier; if an
|
|
SVG marker object, use that marker
|
|
text_attr default={} SVG attributes for the text labels
|
|
attribute=value pairs keyword list SVG attributes
|
|
"""
|
|
defaults = {"stroke-width": "0.25pt", }
|
|
text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
|
|
|
|
def __repr__(self):
|
|
return "<CurveAxis %s [%s, %s] ticks=%s labels=%s %s>" % (
|
|
self.f, self.low, self.high, str(self.ticks), str(self.labels), self.attr)
|
|
|
|
def __init__(self, f, low, high, ticks=-10, miniticks=True, labels=True, logbase=None,
|
|
arrow_start=None, arrow_end=None, text_attr={}, **attr):
|
|
tattr = dict(self.text_defaults)
|
|
tattr.update(text_attr)
|
|
Curve.__init__(self, f, low, high)
|
|
Ticks.__init__(self, f, low, high, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
func = Curve.SVG(self, trans)
|
|
ticks = Ticks.SVG(self, trans) # returns a <g />
|
|
|
|
if self.arrow_start != False and self.arrow_start is not None:
|
|
if isinstance(self.arrow_start, basestring):
|
|
func.attr["marker-start"] = "url(#%s)" % self.arrow_start
|
|
else:
|
|
func.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
|
|
|
|
if self.arrow_end != False and self.arrow_end is not None:
|
|
if isinstance(self.arrow_end, basestring):
|
|
func.attr["marker-end"] = "url(#%s)" % self.arrow_end
|
|
else:
|
|
func.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
|
|
|
|
ticks.append(func)
|
|
return ticks
|
|
|
|
|
|
class LineAxis(Line, Ticks):
|
|
"""Draws an axis with tick marks along a line.
|
|
|
|
LineAxis(x1, y1, x2, y2, start, end, ticks, miniticks, labels, logbase,
|
|
arrow_start, arrow_end, text_attr, attribute=value)
|
|
|
|
x1, y1 required starting point
|
|
x2, y2 required ending point
|
|
start, end default=0, 1 values to start and end labeling
|
|
ticks default=-10 request ticks according to the standard
|
|
tick specification (see help(Ticks))
|
|
miniticks default=True request miniticks according to the
|
|
standard minitick specification
|
|
labels True request tick labels according to the
|
|
standard tick label specification
|
|
logbase default=None if a number, the x axis is logarithmic
|
|
with ticks at the given base (usually 10)
|
|
arrow_start default=None if a new string identifier, draw an arrow
|
|
at the low-end of the axis, referenced by
|
|
that identifier; if an SVG marker object,
|
|
use that marker
|
|
arrow_end default=None if a new string identifier, draw an arrow
|
|
at the high-end of the axis, referenced by
|
|
that identifier; if an SVG marker object,
|
|
use that marker
|
|
text_attr default={} SVG attributes for the text labels
|
|
attribute=value pairs keyword list SVG attributes
|
|
"""
|
|
defaults = {"stroke-width": "0.25pt", }
|
|
text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
|
|
|
|
def __repr__(self):
|
|
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)
|
|
|
|
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):
|
|
self.start = start
|
|
self.end = end
|
|
self.exclude = exclude
|
|
tattr = dict(self.text_defaults)
|
|
tattr.update(text_attr)
|
|
Line.__init__(self, x1, y1, x2, y2, **attr)
|
|
Ticks.__init__(self, None, None, None, ticks, miniticks, labels, logbase, arrow_start, arrow_end, tattr, **attr)
|
|
|
|
def interpret(self):
|
|
if self.exclude is not None and not (isinstance(self.exclude, (tuple, list)) and len(self.exclude) == 2 and
|
|
isinstance(self.exclude[0], (int, long, float)) and isinstance(self.exclude[1], (int, long, float))):
|
|
raise TypeError("exclude must either be None or (low, high)")
|
|
|
|
ticks, miniticks = Ticks.interpret(self)
|
|
if self.exclude is None:
|
|
return ticks, miniticks
|
|
|
|
ticks2 = {}
|
|
for loc, label in ticks.items():
|
|
if self.exclude[0] <= loc <= self.exclude[1]:
|
|
ticks2[loc] = ""
|
|
else:
|
|
ticks2[loc] = label
|
|
|
|
return ticks2, miniticks
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
line = Line.SVG(self, trans) # must be evaluated first, to set self.f, self.low, self.high
|
|
|
|
f01 = self.f
|
|
self.f = lambda t: f01(1. * (t - self.start) / (self.end - self.start))
|
|
self.low = self.start
|
|
self.high = self.end
|
|
|
|
if self.arrow_start != False and self.arrow_start is not None:
|
|
if isinstance(self.arrow_start, basestring):
|
|
line.attr["marker-start"] = "url(#%s)" % self.arrow_start
|
|
else:
|
|
line.attr["marker-start"] = "url(#%s)" % self.arrow_start.id
|
|
|
|
if self.arrow_end != False and self.arrow_end is not None:
|
|
if isinstance(self.arrow_end, basestring):
|
|
line.attr["marker-end"] = "url(#%s)" % self.arrow_end
|
|
else:
|
|
line.attr["marker-end"] = "url(#%s)" % self.arrow_end.id
|
|
|
|
ticks = Ticks.SVG(self, trans) # returns a <g />
|
|
ticks.append(line)
|
|
return ticks
|
|
|
|
|
|
class XAxis(LineAxis):
|
|
"""Draws an x axis with tick marks.
|
|
|
|
XAxis(xmin, xmax, aty, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
|
|
exclude, text_attr, attribute=value)
|
|
|
|
xmin, xmax required the x range
|
|
aty default=0 y position to draw the axis
|
|
ticks default=-10 request ticks according to the standard
|
|
tick specification (see help(Ticks))
|
|
miniticks default=True request miniticks according to the
|
|
standard minitick specification
|
|
labels True request tick labels according to the
|
|
standard tick label specification
|
|
logbase default=None if a number, the x axis is logarithmic
|
|
with ticks at the given base (usually 10)
|
|
arrow_start default=None if a new string identifier, draw an arrow
|
|
at the low-end of the axis, referenced by
|
|
that identifier; if an SVG marker object,
|
|
use that marker
|
|
arrow_end default=None if a new string identifier, draw an arrow
|
|
at the high-end of the axis, referenced by
|
|
that identifier; if an SVG marker object,
|
|
use that marker
|
|
exclude default=None if a (low, high) pair, don't draw text
|
|
labels within this range
|
|
text_attr default={} SVG attributes for the text labels
|
|
attribute=value pairs keyword list SVG attributes for all lines
|
|
|
|
The exclude option is provided for Axes to keep text from overlapping
|
|
where the axes cross. Normal users are not likely to need it.
|
|
"""
|
|
defaults = {"stroke-width": "0.25pt", }
|
|
text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, "dominant-baseline": "text-before-edge", }
|
|
text_start = -1.
|
|
text_angle = 0.
|
|
|
|
def __repr__(self):
|
|
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) # XXX self.xmin/xmax undefd!
|
|
|
|
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):
|
|
self.aty = aty
|
|
tattr = dict(self.text_defaults)
|
|
tattr.update(text_attr)
|
|
LineAxis.__init__(self, xmin, aty, xmax, aty, xmin, xmax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
self.y1 = self.aty
|
|
self.y2 = self.aty
|
|
return LineAxis.SVG(self, trans)
|
|
|
|
|
|
class YAxis(LineAxis):
|
|
"""Draws a y axis with tick marks.
|
|
|
|
YAxis(ymin, ymax, atx, ticks, miniticks, labels, logbase, arrow_start, arrow_end,
|
|
exclude, text_attr, attribute=value)
|
|
|
|
ymin, ymax required the y range
|
|
atx default=0 x position to draw the axis
|
|
ticks default=-10 request ticks according to the standard
|
|
tick specification (see help(Ticks))
|
|
miniticks default=True request miniticks according to the
|
|
standard minitick specification
|
|
labels True request tick labels according to the
|
|
standard tick label specification
|
|
logbase default=None if a number, the y axis is logarithmic
|
|
with ticks at the given base (usually 10)
|
|
arrow_start default=None if a new string identifier, draw an arrow
|
|
at the low-end of the axis, referenced by
|
|
that identifier; if an SVG marker object,
|
|
use that marker
|
|
arrow_end default=None if a new string identifier, draw an arrow
|
|
at the high-end of the axis, referenced by
|
|
that identifier; if an SVG marker object,
|
|
use that marker
|
|
exclude default=None if a (low, high) pair, don't draw text
|
|
labels within this range
|
|
text_attr default={} SVG attributes for the text labels
|
|
attribute=value pairs keyword list SVG attributes for all lines
|
|
|
|
The exclude option is provided for Axes to keep text from overlapping
|
|
where the axes cross. Normal users are not likely to need it.
|
|
"""
|
|
defaults = {"stroke-width": "0.25pt", }
|
|
text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, "text-anchor": "end", "dominant-baseline": "middle", }
|
|
text_start = 2.5
|
|
text_angle = 90.
|
|
|
|
def __repr__(self):
|
|
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) # XXX self.ymin/ymax undefd!
|
|
|
|
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):
|
|
self.atx = atx
|
|
tattr = dict(self.text_defaults)
|
|
tattr.update(text_attr)
|
|
LineAxis.__init__(self, atx, ymin, atx, ymax, ymin, ymax, ticks, miniticks, labels, logbase, arrow_start, arrow_end, exclude, tattr, **attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
self.x1 = self.atx
|
|
self.x2 = self.atx
|
|
return LineAxis.SVG(self, trans)
|
|
|
|
|
|
class Axes:
|
|
"""Draw a pair of intersecting x-y axes.
|
|
|
|
Axes(xmin, xmax, ymin, ymax, atx, aty, xticks, xminiticks, xlabels, xlogbase,
|
|
yticks, yminiticks, ylabels, ylogbase, arrows, text_attr, attribute=value)
|
|
|
|
xmin, xmax required the x range
|
|
ymin, ymax required the y range
|
|
atx, aty default=0, 0 point where the axes try to cross;
|
|
if outside the range, the axes will
|
|
cross at the closest corner
|
|
xticks default=-10 request ticks according to the standard
|
|
tick specification (see help(Ticks))
|
|
xminiticks default=True request miniticks according to the
|
|
standard minitick specification
|
|
xlabels True request tick labels according to the
|
|
standard tick label specification
|
|
xlogbase default=None if a number, the x axis is logarithmic
|
|
with ticks at the given base (usually 10)
|
|
yticks default=-10 request ticks according to the standard
|
|
tick specification
|
|
yminiticks default=True request miniticks according to the
|
|
standard minitick specification
|
|
ylabels True request tick labels according to the
|
|
standard tick label specification
|
|
ylogbase default=None if a number, the y axis is logarithmic
|
|
with ticks at the given base (usually 10)
|
|
arrows default=None if a new string identifier, draw arrows
|
|
referenced by that identifier
|
|
text_attr default={} SVG attributes for the text labels
|
|
attribute=value pairs keyword list SVG attributes for all lines
|
|
"""
|
|
defaults = {"stroke-width": "0.25pt", }
|
|
text_defaults = {"stroke": "none", "fill": "black", "font-size": 5, }
|
|
|
|
def __repr__(self):
|
|
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)
|
|
|
|
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):
|
|
self.xmin, self.xmax = xmin, xmax
|
|
self.ymin, self.ymax = ymin, ymax
|
|
self.atx, self.aty = atx, aty
|
|
self.xticks, self.xminiticks, self.xlabels, self.xlogbase = xticks, xminiticks, xlabels, xlogbase
|
|
self.yticks, self.yminiticks, self.ylabels, self.ylogbase = yticks, yminiticks, ylabels, ylogbase
|
|
self.arrows = arrows
|
|
|
|
self.text_attr = dict(self.text_defaults)
|
|
self.text_attr.update(text_attr)
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
atx, aty = self.atx, self.aty
|
|
if atx < self.xmin:
|
|
atx = self.xmin
|
|
if atx > self.xmax:
|
|
atx = self.xmax
|
|
if aty < self.ymin:
|
|
aty = self.ymin
|
|
if aty > self.ymax:
|
|
aty = self.ymax
|
|
|
|
xmargin = 0.1 * abs(self.ymin - self.ymax)
|
|
xexclude = atx - xmargin, atx + xmargin
|
|
|
|
ymargin = 0.1 * abs(self.xmin - self.xmax)
|
|
yexclude = aty - ymargin, aty + ymargin
|
|
|
|
if self.arrows is not None and self.arrows != False:
|
|
xarrow_start = self.arrows + ".xstart"
|
|
xarrow_end = self.arrows + ".xend"
|
|
yarrow_start = self.arrows + ".ystart"
|
|
yarrow_end = self.arrows + ".yend"
|
|
else:
|
|
xarrow_start = xarrow_end = yarrow_start = yarrow_end = None
|
|
|
|
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)
|
|
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)
|
|
return SVG("g", *(xaxis.sub + yaxis.sub))
|
|
|
|
######################################################################
|
|
|
|
class HGrid(Ticks):
|
|
"""Draws the horizontal lines of a grid over a specified region
|
|
using the standard tick specification (see help(Ticks)) to place the
|
|
grid lines.
|
|
|
|
HGrid(xmin, xmax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
|
|
|
|
xmin, xmax required the x range
|
|
low, high required the y range
|
|
ticks default=-10 request ticks according to the standard
|
|
tick specification (see help(Ticks))
|
|
miniticks default=False request miniticks according to the
|
|
standard minitick specification
|
|
logbase default=None if a number, the axis is logarithmic
|
|
with ticks at the given base (usually 10)
|
|
mini_attr default={} SVG attributes for the minitick-lines
|
|
(if miniticks != False)
|
|
attribute=value pairs keyword list SVG attributes for the major tick lines
|
|
"""
|
|
defaults = {"stroke-width": "0.25pt", "stroke": "gray", }
|
|
mini_defaults = {"stroke-width": "0.25pt", "stroke": "lightgray", "stroke-dasharray": "1,1", }
|
|
|
|
def __repr__(self):
|
|
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)
|
|
|
|
def __init__(self, xmin, xmax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
|
|
self.xmin, self.xmax = xmin, xmax
|
|
|
|
self.mini_attr = dict(self.mini_defaults)
|
|
self.mini_attr.update(mini_attr)
|
|
|
|
Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
self.last_ticks, self.last_miniticks = Ticks.interpret(self)
|
|
|
|
ticksd = []
|
|
for t in self.last_ticks.keys():
|
|
ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
|
|
|
|
miniticksd = []
|
|
for t in self.last_miniticks:
|
|
miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
|
|
|
|
return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
|
|
|
|
|
|
class VGrid(Ticks):
|
|
"""Draws the vertical lines of a grid over a specified region
|
|
using the standard tick specification (see help(Ticks)) to place the
|
|
grid lines.
|
|
|
|
HGrid(ymin, ymax, low, high, ticks, miniticks, logbase, mini_attr, attribute=value)
|
|
|
|
ymin, ymax required the y range
|
|
low, high required the x range
|
|
ticks default=-10 request ticks according to the standard
|
|
tick specification (see help(Ticks))
|
|
miniticks default=False request miniticks according to the
|
|
standard minitick specification
|
|
logbase default=None if a number, the axis is logarithmic
|
|
with ticks at the given base (usually 10)
|
|
mini_attr default={} SVG attributes for the minitick-lines
|
|
(if miniticks != False)
|
|
attribute=value pairs keyword list SVG attributes for the major tick lines
|
|
"""
|
|
defaults = {"stroke-width": "0.25pt", "stroke": "gray", }
|
|
mini_defaults = {"stroke-width": "0.25pt", "stroke": "lightgray", "stroke-dasharray": "1,1", }
|
|
|
|
def __repr__(self):
|
|
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)
|
|
|
|
def __init__(self, ymin, ymax, low, high, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
|
|
self.ymin, self.ymax = ymin, ymax
|
|
|
|
self.mini_attr = dict(self.mini_defaults)
|
|
self.mini_attr.update(mini_attr)
|
|
|
|
Ticks.__init__(self, None, low, high, ticks, miniticks, None, logbase)
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
self.last_ticks, self.last_miniticks = Ticks.interpret(self)
|
|
|
|
ticksd = []
|
|
for t in self.last_ticks.keys():
|
|
ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
|
|
|
|
miniticksd = []
|
|
for t in self.last_miniticks:
|
|
miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
|
|
|
|
return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
|
|
|
|
|
|
class Grid(Ticks):
|
|
"""Draws a grid over a specified region using the standard tick
|
|
specification (see help(Ticks)) to place the grid lines.
|
|
|
|
Grid(xmin, xmax, ymin, ymax, ticks, miniticks, logbase, mini_attr, attribute=value)
|
|
|
|
xmin, xmax required the x range
|
|
ymin, ymax required the y range
|
|
ticks default=-10 request ticks according to the standard
|
|
tick specification (see help(Ticks))
|
|
miniticks default=False request miniticks according to the
|
|
standard minitick specification
|
|
logbase default=None if a number, the axis is logarithmic
|
|
with ticks at the given base (usually 10)
|
|
mini_attr default={} SVG attributes for the minitick-lines
|
|
(if miniticks != False)
|
|
attribute=value pairs keyword list SVG attributes for the major tick lines
|
|
"""
|
|
defaults = {"stroke-width": "0.25pt", "stroke": "gray", }
|
|
mini_defaults = {"stroke-width": "0.25pt", "stroke": "lightgray", "stroke-dasharray": "1,1", }
|
|
|
|
def __repr__(self):
|
|
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)
|
|
|
|
def __init__(self, xmin, xmax, ymin, ymax, ticks=-10, miniticks=False, logbase=None, mini_attr={}, **attr):
|
|
self.xmin, self.xmax = xmin, xmax
|
|
self.ymin, self.ymax = ymin, ymax
|
|
|
|
self.mini_attr = dict(self.mini_defaults)
|
|
self.mini_attr.update(mini_attr)
|
|
|
|
Ticks.__init__(self, None, None, None, ticks, miniticks, None, logbase)
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
self.low, self.high = self.xmin, self.xmax
|
|
self.last_xticks, self.last_xminiticks = Ticks.interpret(self)
|
|
self.low, self.high = self.ymin, self.ymax
|
|
self.last_yticks, self.last_yminiticks = Ticks.interpret(self)
|
|
|
|
ticksd = []
|
|
for t in self.last_xticks.keys():
|
|
ticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
|
|
for t in self.last_yticks.keys():
|
|
ticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
|
|
|
|
miniticksd = []
|
|
for t in self.last_xminiticks:
|
|
miniticksd += Line(t, self.ymin, t, self.ymax).Path(trans).d
|
|
for t in self.last_yminiticks:
|
|
miniticksd += Line(self.xmin, t, self.xmax, t).Path(trans).d
|
|
|
|
return SVG("g", Path(d=ticksd, **self.attr).SVG(), Path(d=miniticksd, **self.mini_attr).SVG())
|
|
|
|
######################################################################
|
|
|
|
class XErrorBars:
|
|
"""Draws x error bars at a set of points. This is usually used
|
|
before (under) a set of Dots at the same points.
|
|
|
|
XErrorBars(d, attribute=value)
|
|
|
|
d required list of (x,y,xerr...) points
|
|
attribute=value pairs keyword list SVG attributes
|
|
|
|
If points in d have
|
|
|
|
* 3 elements, the third is the symmetric error bar
|
|
* 4 elements, the third and fourth are the asymmetric lower and
|
|
upper error bar. The third element should be negative,
|
|
e.g. (5, 5, -1, 2) is a bar from 4 to 7.
|
|
* more than 4, a tick mark is placed at each value. This lets
|
|
you nest errors from different sources, correlated and
|
|
uncorrelated, statistical and systematic, etc.
|
|
"""
|
|
defaults = {"stroke-width": "0.25pt", }
|
|
|
|
def __repr__(self):
|
|
return "<XErrorBars (%d nodes)>" % len(self.d)
|
|
|
|
def __init__(self, d=[], **attr):
|
|
self.d = list(d)
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans) # only once
|
|
|
|
output = SVG("g")
|
|
for p in self.d:
|
|
x, y = p[0], p[1]
|
|
|
|
if len(p) == 3:
|
|
bars = [x - p[2], x + p[2]]
|
|
else:
|
|
bars = [x + pi for pi in p[2:]]
|
|
|
|
start, end = min(bars), max(bars)
|
|
output.append(LineAxis(start, y, end, y, start, end, bars, False, False, **self.attr).SVG(trans))
|
|
|
|
return output
|
|
|
|
|
|
class YErrorBars:
|
|
"""Draws y error bars at a set of points. This is usually used
|
|
before (under) a set of Dots at the same points.
|
|
|
|
YErrorBars(d, attribute=value)
|
|
|
|
d required list of (x,y,yerr...) points
|
|
attribute=value pairs keyword list SVG attributes
|
|
|
|
If points in d have
|
|
|
|
* 3 elements, the third is the symmetric error bar
|
|
* 4 elements, the third and fourth are the asymmetric lower and
|
|
upper error bar. The third element should be negative,
|
|
e.g. (5, 5, -1, 2) is a bar from 4 to 7.
|
|
* more than 4, a tick mark is placed at each value. This lets
|
|
you nest errors from different sources, correlated and
|
|
uncorrelated, statistical and systematic, etc.
|
|
"""
|
|
defaults = {"stroke-width": "0.25pt", }
|
|
|
|
def __repr__(self):
|
|
return "<YErrorBars (%d nodes)>" % len(self.d)
|
|
|
|
def __init__(self, d=[], **attr):
|
|
self.d = list(d)
|
|
|
|
self.attr = dict(self.defaults)
|
|
self.attr.update(attr)
|
|
|
|
def SVG(self, trans=None):
|
|
"""Apply the transformation "trans" and return an SVG object."""
|
|
if isinstance(trans, basestring):
|
|
trans = totrans(trans) # only once
|
|
|
|
output = SVG("g")
|
|
for p in self.d:
|
|
x, y = p[0], p[1]
|
|
|
|
if len(p) == 3:
|
|
bars = [y - p[2], y + p[2]]
|
|
else:
|
|
bars = [y + pi for pi in p[2:]]
|
|
|
|
start, end = min(bars), max(bars)
|
|
output.append(LineAxis(x, start, x, end, start, end, bars, False, False, **self.attr).SVG(trans))
|
|
|
|
return output
|