CMS 3D CMS Logo

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