svgfig.py 149 KB

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