svgfig.py 148 KB

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