2024-06-18 15:38:57 +08:00
# BSD 3-Clause License
# Copyright (c) 2022, Jim Pivarski
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2011-02-24 02:11:25 +08:00
import re , codecs , os , platform , copy , itertools , math , cmath , random , sys , copy
_epsilon = 1e-5
2017-01-16 18:03:49 +08:00
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 )
2011-02-24 02:11:25 +08:00
2024-06-18 15:38:57 +08:00
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 "
2018-05-03 15:19:05 +08:00
2011-02-24 02:11:25 +08:00
_default_fileName = " tmp.svg "
_hacks = { }
_hacks [ " inkscape-text-vertical-shift " ] = False
2024-06-18 15:38:57 +08:00
__version__ = " 1.0.1 "
2011-02-24 02:11:25 +08:00
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 ) :
2017-01-16 18:03:49 +08:00
attrCopy = attr . copy ( )
2011-02-24 02:11:25 +08:00
for name in attr . keys ( ) :
name_colon = re . sub ( " __ " , " : " , name )
if name_colon != name :
2017-01-16 18:03:49 +08:00
attrCopy [ name_colon ] = attrCopy [ name ]
del attrCopy [ name ]
2011-02-24 02:11:25 +08:00
name = name_colon
name_dash = re . sub ( " _ " , " - " , name )
if name_dash != name :
2017-01-16 18:03:49 +08:00
attrCopy [ name_dash ] = attrCopy [ name ]
del attrCopy [ name ]
2011-02-24 02:11:25 +08:00
name = name_dash
2017-01-16 18:03:49 +08:00
return attrCopy
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " SVG element must have a t (SVG type) " )
2011-02-24 02:11:25 +08:00
# 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 . """
2017-01-16 18:03:49 +08:00
raise NotImplementedError ( " Got an algorithm for breadth-first searching a tree without effectively copying the tree? " )
2011-02-24 02:11:25 +08:00
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 " % ( ( " %% - %d s " % tree_width ) % repr ( None ) ,
( " %% - %d s " % 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 " % ( ( " %% - %d s " % tree_width ) % repr ( list ( ti ) ) ,
( " %% - %d s " % 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 " >
""" % e ncoding + (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
2024-06-18 15:38:57 +08:00
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
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( " canvas viewBox is incorrectly formatted " )
2011-02-24 02:11:25 +08:00
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. """
2024-06-18 15:38:57 +08:00
return load_stream ( file ( fileName ) )
2011-02-24 02:11:25 +08:00
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 * 1 j ) )
set_func_name ( output , expr . func_name )
return output
else :
2017-01-16 18:03:49 +08:00
raise TypeError ( " must be a function of 2 or 1 variables " )
2011-02-24 02:11:25 +08:00
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 * 1 j ) )
set_func_name ( output2 , " %s -> %s " % ( vars [ 0 ] , expr ) )
return output2
else :
2017-01-16 18:03:49 +08:00
raise TypeError ( " vars must have 2 or 1 elements " )
2011-02-24 02:11:25 +08:00
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. ) :
2017-01-16 18:03:49 +08:00
raise ValueError ( " x range incompatible with log scaling: ( %g , %g ) " % ( ix1 , ix2 ) )
2011-02-24 02:11:25 +08:00
if ylogbase is not None and ( iy1 < = 0. or iy2 < = 0. ) :
2017-01-16 18:03:49 +08:00
raise ValueError ( " y range incompatible with log scaling: ( %g , %g ) " % ( iy1 , iy2 ) )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " Fig() got unexpected keyword arguments %s " % kwds . keys ( ) )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " Plot() got unexpected keyword arguments %s " % kwds . keys ( ) )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " Frame() got unexpected keyword arguments %s " % kwds . keys ( ) )
2011-02-24 02:11:25 +08:00
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 " :
2017-01-16 18:03:49 +08:00
raise TypeError ( " Only SVG <path /> objects can be converted into Paths " )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( errstring )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( errstring )
2011-02-24 02:11:25 +08:00
while num1 is not None :
if num2 is None :
2017-01-16 18:03:49 +08:00
raise ValueError ( errstring )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( errstring )
2011-02-24 02:11:25 +08:00
while num1 is not None :
if num2 is None or num3 is None or num4 is None :
2017-01-16 18:03:49 +08:00
raise ValueError ( errstring )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( errstring )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( errstring )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( errstring )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( errstring )
2011-02-24 02:11:25 +08:00
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 ) ) :
2017-01-16 18:03:49 +08:00
raise TypeError ( " pathdata elements must be tuples/lists " )
2011-02-24 02:11:25 +08:00
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 %g A %g %g %g 0 0 %g %g A %g %g %g 0 0 %g %g A %g %g %g 0 0 %g %g A %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 ) :
2017-01-16 18:03:49 +08:00
raise ValueError ( " low must be less than high " )
2011-02-24 02:11:25 +08:00
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
2024-06-18 15:38:57 +08:00
preceed ( x , y ) , as in SVG paths ) . If ( c1x , c1y ) and
2011-02-24 02:11:25 +08:00
( 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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( " mode must be \" lines \" , \" bezier \" , \" velocity \" , \" foreback \" , \" smooth \" , or an abbreviation " )
2011-02-24 02:11:25 +08:00
d = [ ]
2017-01-16 18:03:49 +08:00
indexes = list ( range ( len ( self . d ) ) )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " arrow_start must be False/None or an id string for the new marker " )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " arrow_end must be False/None or an id string for the new marker " )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " arrow_start must be False/None or an id string for the new marker " )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " arrow_end must be False/None or an id string for the new marker " )
2011-02-24 02:11:25 +08:00
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 " \u00d7 10 "
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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " arrow_start must be False/None or an id string for the new marker " )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " arrow_end must be False/None or an id string for the new marker " )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " labels must be None/False, True, a format string, or a number->string function " )
2011-02-24 02:11:25 +08:00
# 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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " miniticks must be None/False, True, a number of desired miniticks, or a list of numbers " )
2011-02-24 02:11:25 +08:00
# 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 :
2017-01-16 18:03:49 +08:00
raise TypeError ( " miniticks must be None/False, True, a number of desired miniticks, or a list of numbers " )
2011-02-24 02:11:25 +08:00
else :
2017-01-16 18:03:49 +08:00
raise TypeError ( " ticks must be None/False, a number of desired ticks, a list of numbers, or a dictionary of explicit markers " )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( " low must be less than high " )
2011-02-24 02:11:25 +08:00
if N == 1 :
2017-01-16 18:03:49 +08:00
raise ValueError ( " N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum " )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( " original_ticks { %g ... %g } extend beyond [ %g , %g ] " % ( original_ticks [ 0 ] , original_ticks [ - 1 ] , self . low , self . high ) )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( " low must be less than high " )
2011-02-24 02:11:25 +08:00
if N == 1 :
2017-01-16 18:03:49 +08:00
raise ValueError ( " N can be 0 or >1 to specify the exact number of ticks or negative to specify a maximum " )
2011-02-24 02:11:25 +08:00
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 :
2017-01-16 18:03:49 +08:00
raise ValueError ( " low must be less than high " )
2011-02-24 02:11:25 +08:00
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 ) ) ) :
2017-01-16 18:03:49 +08:00
raise TypeError ( " exclude must either be None or (low, high) " )
2011-02-24 02:11:25 +08:00
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