splitaxis, baraxis, baraxispainter, and bar style: minor cleanups and documentation
[PyX/mjg.git] / pyx / graph.py
blob2507b3207df6f08e4c248ff4cda9142425f18370
1 #!/usr/bin/env python
4 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 import types, re, math, string, sys
25 import bbox, canvas, path, tex, unit, mathtree, trafo, attrlist, color
28 goldenrule = 0.5 * (math.sqrt(5) + 1)
30 class _nodefault: pass
33 ################################################################################
34 # some general helper routines
35 ################################################################################
38 def _isstring(arg):
39 "arg is string-like (cf. python cookbook 3.2)"
40 try: arg + ''
41 except: return None
42 return 1
45 def _isnumber(arg):
46 "arg is number-like"
47 try: arg + 0
48 except: return None
49 return 1
52 def _isinteger(arg):
53 "arg is integer-like"
54 try:
55 if type(arg + 0.0) is type(arg):
56 return None
57 return 1
58 except: return None
61 def _issequence(arg):
62 """arg is sequence-like (e.g. has a len)
63 a string is *not* considered to be a sequence"""
64 if _isstring(arg): return None
65 try: len(arg)
66 except: return None
67 return 1
70 def _ensuresequence(arg):
71 """return arg or (arg,) depending on the result of _issequence,
72 None is converted to ()"""
73 if _isstring(arg): return (arg,)
74 if arg is None: return ()
75 if _issequence(arg): return arg
76 return (arg,)
79 def _getitemno(arg, n):
80 if _issequence(arg):
81 try: return arg[n]
82 except: return None
83 else:
84 return arg
87 def _issequenceofsequences(arg):
88 """check if arg has a sequence or None as it's first entry"""
89 return _issequence(arg) and len(arg) and (_issequence(arg[0]) or arg[0] is None)
92 def _getsequenceno(arg, n):
93 """get sequence number n if arg is a sequence of sequences,
94 otherwise it gets just arg"""
95 if _issequenceofsequences(arg):
96 try: return arg[n]
97 except: return None
98 else:
99 return arg
103 ################################################################################
104 # maps
105 ################################################################################
107 class _Imap:
108 "maps convert a value into another value by bijective transformation f"
110 def convert(self, x):
111 "returns f(x)"
113 def invert(self, y):
114 "returns f^-1(y) where f^-1 is the inverse transformation (x=f^-1(f(x)) for all x)"
116 def setbasepoint(self, basepoints):
117 """set basepoints for the convertions
118 basepoints are tuples (x, y) with y == f(x) and x == f^-1(y)
119 the number of basepoints needed might depend on the transformation
120 usually two pairs are needed like for linear maps, logarithmic maps, etc."""
123 class _linmap:
124 "linear mapping"
125 __implements__ = _Imap
127 def setbasepoints(self, basepoints):
128 self.dydx = (basepoints[1][1] - basepoints[0][1]) / float(basepoints[1][0] - basepoints[0][0])
129 self.dxdy = (basepoints[1][0] - basepoints[0][0]) / float(basepoints[1][1] - basepoints[0][1])
130 self.x1 = basepoints[0][0]
131 self.y1 = basepoints[0][1]
132 return self
134 def convert(self, value):
135 return self.y1 + self.dydx * (value - self.x1)
137 def invert(self, value):
138 return self.x1 + self.dxdy * (value - self.y1)
141 class _logmap:
142 "logarithmic mapping"
143 __implements__ = _Imap
145 def setbasepoints(self, basepoints):
146 self.dydx = ((basepoints[1][1] - basepoints[0][1]) /
147 float(math.log(basepoints[1][0]) - math.log(basepoints[0][0])))
148 self.dxdy = ((math.log(basepoints[1][0]) - math.log(basepoints[0][0])) /
149 float(basepoints[1][1] - basepoints[0][1]))
150 self.x1 = math.log(basepoints[0][0])
151 self.y1 = basepoints[0][1]
152 return self
154 def convert(self, value):
155 return self.y1 + self.dydx * (math.log(value) - self.x1)
157 def invert(self, value):
158 return math.exp(self.x1 + self.dxdy * (value - self.y1))
162 ################################################################################
163 # tick lists = partitions
164 ################################################################################
167 class frac:
168 "fraction type for rational arithmetics"
170 def __init__(self, enum, denom, power=None):
171 "for power!=None: frac=(enum/denom)**power"
172 if not _isinteger(enum) or not _isinteger(denom): raise TypeError("integer type expected")
173 if not denom: raise ZeroDivisionError("zero denominator")
174 if power != None:
175 if not _isinteger(power): raise TypeError("integer type expected")
176 if power >= 0:
177 self.enum = long(enum) ** power
178 self.denom = long(denom) ** power
179 else:
180 self.enum = long(denom) ** (-power)
181 self.denom = long(enum) ** (-power)
182 else:
183 self.enum = enum
184 self.denom = denom
186 def __cmp__(self, other):
187 if other is None:
188 return 1
189 return cmp(self.enum * other.denom, other.enum * self.denom)
191 def __mul__(self, other):
192 return frac(self.enum * other.enum, self.denom * other.denom)
194 def __float__(self):
195 return float(self.enum) / self.denom
197 def __str__(self):
198 return "%i/%i" % (self.enum, self.denom)
200 def __repr__(self):
201 return "frac(%r, %r)" % (self.enum, self.denom) # I want to see the "L"
204 def _ensurefrac(arg):
205 "ensure frac by converting a string to frac"
207 def createfrac(str):
208 commaparts = str.split(".")
209 for part in commaparts:
210 if not part.isdigit(): raise ValueError("non-digits found in '%s'" % part)
211 if len(commaparts) == 1:
212 return frac(long(commaparts[0]), 1)
213 elif len(commaparts) == 2:
214 result = frac(1, 10l, power=len(commaparts[1]))
215 result.enum = long(commaparts[0])*result.denom + long(commaparts[1])
216 return result
217 else: raise ValueError("multiple '.' found in '%s'" % str)
219 if _isstring(arg):
220 fraction = arg.split("/")
221 if len(fraction) > 2: raise ValueError("multiple '/' found in '%s'" % arg)
222 value = createfrac(fraction[0])
223 if len(fraction) == 2:
224 value2 = createfrac(fraction[1])
225 value = frac(value.enum * value2.denom, value.denom * value2.enum)
226 return value
227 if not isinstance(arg, frac): raise ValueError("can't convert argument to frac")
228 return arg
231 class tick(frac):
232 "a tick is a frac enhanced by a ticklevel, a labellevel and a text (they all might be None)"
234 def __init__(self, enum, denom, ticklevel=None, labellevel=None, text=None):
235 frac.__init__(self, enum, denom)
236 self.ticklevel = ticklevel
237 self.labellevel = labellevel
238 self.text = text
240 def merge(self, other):
241 if self.ticklevel is None or (other.ticklevel is not None and other.ticklevel < self.ticklevel):
242 self.ticklevel = other.ticklevel
243 if self.labellevel is None or (other.labellevel is not None and other.labellevel < self.labellevel):
244 self.labellevel = other.labellevel
245 if self.text is None:
246 self.text = other.text
248 def __repr__(self):
249 return "tick(%r, %r, %s, %s, %s)" % (self.enum, self.denom, self.ticklevel, self.labellevel, self.text)
252 def _mergeticklists(list1, list2):
253 """return a merged list of ticks out of list1 and list2
254 lists have to be ordered (returned list is also ordered)
255 caution: side effects (input lists might be altered)"""
256 # TODO: improve this using bisect
257 i = 0
258 j = 0
259 try:
260 while 1: # we keep on going until we reach an index error
261 while list2[j] < list1[i]: # insert tick
262 list1.insert(i, list2[j])
263 i += 1
264 j += 1
265 if list2[j] == list1[i]: # merge tick
266 list1[i].merge(list2[j])
267 j += 1
268 i += 1
269 except IndexError:
270 if j < len(list2):
271 list1 += list2[j:]
272 return list1
275 def _mergetexts(ticks, texts):
276 "merges texts into ticks"
277 if _issequenceofsequences(texts):
278 for text, level in zip(texts, xrange(sys.maxint)):
279 usetext = _ensuresequence(text)
280 i = 0
281 for tick in ticks:
282 if tick.labellevel == level:
283 tick.text = usetext[i]
284 i += 1
285 if i != len(usetext):
286 raise IndexError("wrong sequence length of texts at level %i" % level)
287 elif texts is not None:
288 usetext = _ensuresequence(texts)
289 i = 0
290 for tick in ticks:
291 if tick.labellevel == 0:
292 tick.text = usetext[i]
293 i += 1
294 if i != len(usetext):
295 raise IndexError("wrong sequence length of texts")
298 class manualpart:
300 def __init__(self, ticks=None, labels=None, texts=None, mix=()):
301 self.multipart = 0
302 if ticks is None and labels is not None:
303 self.ticks = _ensuresequence(_getsequenceno(labels, 0))
304 else:
305 self.ticks = ticks
307 if labels is None and ticks is not None:
308 self.labels = _ensuresequence(_getsequenceno(ticks, 0))
309 else:
310 self.labels = labels
311 self.texts = texts
312 self.mix = mix
315 def checkfraclist(self, *fracs):
316 if not len(fracs): return ()
317 sorted = list(fracs)
318 sorted.sort()
319 last = sorted[0]
320 for item in sorted[1:]:
321 if last == item:
322 raise ValueError("duplicate entry found")
323 last = item
324 return sorted
326 def part(self):
327 ticks = list(self.mix)
328 if _issequenceofsequences(self.ticks):
329 for fracs, level in zip(self.ticks, xrange(sys.maxint)):
330 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, ticklevel = level)
331 for frac in self.checkfraclist(*map(_ensurefrac, _ensuresequence(fracs)))])
332 else:
333 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, ticklevel = 0)
334 for frac in self.checkfraclist(*map(_ensurefrac, _ensuresequence(self.ticks)))])
336 if _issequenceofsequences(self.labels):
337 for fracs, level in zip(self.labels, xrange(sys.maxint)):
338 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, labellevel = level)
339 for frac in self.checkfraclist(*map(_ensurefrac, _ensuresequence(fracs)))])
340 else:
341 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, labellevel = 0)
342 for frac in self.checkfraclist(*map(_ensurefrac, _ensuresequence(self.labels)))])
344 _mergetexts(ticks, self.texts)
346 return ticks
348 def defaultpart(self, min, max, extendmin, extendmax):
349 return self.part()
352 class linpart:
354 def __init__(self, ticks=None, labels=None, texts=None, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
355 self.multipart = 0
356 if ticks is None and labels is not None:
357 self.ticks = (_ensurefrac(_ensuresequence(labels)[0]),)
358 else:
359 self.ticks = map(_ensurefrac, _ensuresequence(ticks))
360 if labels is None and ticks is not None:
361 self.labels = (_ensurefrac(_ensuresequence(ticks)[0]),)
362 else:
363 self.labels = map(_ensurefrac, _ensuresequence(labels))
364 self.texts = texts
365 self.extendtick = extendtick
366 self.extendlabel = extendlabel
367 self.epsilon = epsilon
368 self.mix = mix
370 def extendminmax(self, min, max, frac, extendmin, extendmax):
371 if extendmin:
372 min = float(frac) * math.floor(min / float(frac) + self.epsilon)
373 if extendmax:
374 max = float(frac) * math.ceil(max / float(frac) - self.epsilon)
375 return min, max
377 def getticks(self, min, max, frac, ticklevel=None, labellevel=None):
378 imin = int(math.ceil(min / float(frac) - 0.5 * self.epsilon))
379 imax = int(math.floor(max / float(frac) + 0.5 * self.epsilon))
380 ticks = []
381 for i in range(imin, imax + 1):
382 ticks.append(tick(long(i) * frac.enum, frac.denom, ticklevel = ticklevel, labellevel = labellevel))
383 return ticks
385 def defaultpart(self, min, max, extendmin, extendmax):
386 if self.extendtick is not None and len(self.ticks) > self.extendtick:
387 min, max = self.extendminmax(min, max, self.ticks[self.extendtick], extendmin, extendmax)
388 if self.extendlabel is not None and len(self.labels) > self.extendlabel:
389 min, max = self.extendminmax(min, max, self.labels[self.extendlabel], extendmin, extendmax)
391 ticks = list(self.mix)
392 for i in range(len(self.ticks)):
393 ticks = _mergeticklists(ticks, self.getticks(min, max, self.ticks[i], ticklevel = i))
394 for i in range(len(self.labels)):
395 ticks = _mergeticklists(ticks, self.getticks(min, max, self.labels[i], labellevel = i))
397 _mergetexts(ticks, self.texts)
399 return ticks
402 class autolinpart:
404 defaultlist = ((frac(1, 1), frac(1, 2)),
405 (frac(2, 1), frac(1, 1)),
406 (frac(5, 2), frac(5, 4)),
407 (frac(5, 1), frac(5, 2)))
409 def __init__(self, list=defaultlist, extendtick=0, epsilon=1e-10, mix=()):
410 self.multipart = 1
411 self.list = list
412 self.extendtick = extendtick
413 self.epsilon = epsilon
414 self.mix = mix
416 def defaultpart(self, min, max, extendmin, extendmax):
417 base = frac(10L, 1, int(math.log(max - min) / math.log(10)))
418 ticks = self.list[0]
419 useticks = [tick * base for tick in ticks]
420 self.lesstickindex = self.moretickindex = 0
421 self.lessbase = frac(base.enum, base.denom)
422 self.morebase = frac(base.enum, base.denom)
423 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
424 part = linpart(ticks=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
425 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
427 def lesspart(self):
428 if self.lesstickindex < len(self.list) - 1:
429 self.lesstickindex += 1
430 else:
431 self.lesstickindex = 0
432 self.lessbase.enum *= 10
433 ticks = self.list[self.lesstickindex]
434 useticks = [tick * self.lessbase for tick in ticks]
435 part = linpart(ticks=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
436 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
438 def morepart(self):
439 if self.moretickindex:
440 self.moretickindex -= 1
441 else:
442 self.moretickindex = len(self.list) - 1
443 self.morebase.denom *= 10
444 ticks = self.list[self.moretickindex]
445 useticks = [tick * self.morebase for tick in ticks]
446 part = linpart(ticks=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
447 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
450 class shiftfracs:
452 def __init__(self, shift, *fracs):
453 self.shift = shift
454 self.fracs = fracs
457 class logpart(linpart):
459 shift5fracs1 = shiftfracs(100000, frac(1, 1))
460 shift4fracs1 = shiftfracs(10000, frac(1, 1))
461 shift3fracs1 = shiftfracs(1000, frac(1, 1))
462 shift2fracs1 = shiftfracs(100, frac(1, 1))
463 shiftfracs1 = shiftfracs(10, frac(1, 1))
464 shiftfracs125 = shiftfracs(10, frac(1, 1), frac(2, 1), frac(5, 1))
465 shiftfracs1to9 = shiftfracs(10, *list(map(lambda x: frac(x, 1), range(1, 10))))
466 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
468 def __init__(self, ticks=None, labels=None, texts=None, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
469 self.multipart = 0
470 if ticks is None and labels is not None:
471 self.ticks = (_ensuresequence(labels)[0],)
472 else:
473 self.ticks = _ensuresequence(ticks)
475 if labels is None and ticks is not None:
476 self.labels = (_ensuresequence(ticks)[0],)
477 else:
478 self.labels = _ensuresequence(labels)
479 self.texts = texts
480 self.extendtick = extendtick
481 self.extendlabel = extendlabel
482 self.epsilon = epsilon
483 self.mix = mix
485 def extendminmax(self, min, max, shiftfracs, extendmin, extendmax):
486 minpower = None
487 maxpower = None
488 for i in xrange(len(shiftfracs.fracs)):
489 imin = int(math.floor(math.log(min / float(shiftfracs.fracs[i])) /
490 math.log(shiftfracs.shift) + self.epsilon)) + 1
491 imax = int(math.ceil(math.log(max / float(shiftfracs.fracs[i])) /
492 math.log(shiftfracs.shift) - self.epsilon)) - 1
493 if minpower is None or imin < minpower:
494 minpower, minindex = imin, i
495 if maxpower is None or imax >= maxpower:
496 maxpower, maxindex = imax, i
497 if minindex:
498 minfrac = shiftfracs.fracs[minindex - 1]
499 else:
500 minfrac = shiftfracs.fracs[-1]
501 minpower -= 1
502 if maxindex != len(shiftfracs.fracs) - 1:
503 maxfrac = shiftfracs.fracs[maxindex + 1]
504 else:
505 maxfrac = shiftfracs.fracs[0]
506 maxpower += 1
507 if extendmin:
508 min = float(minfrac) * float(shiftfracs.shift) ** minpower
509 if extendmax:
510 max = float(maxfrac) * float(shiftfracs.shift) ** maxpower
511 return min, max
513 def getticks(self, min, max, shiftfracs, ticklevel=None, labellevel=None):
514 ticks = list(self.mix)
515 minimin = 0
516 maximax = 0
517 for f in shiftfracs.fracs:
518 fracticks = []
519 imin = int(math.ceil(math.log(min / float(f)) /
520 math.log(shiftfracs.shift) - 0.5 * self.epsilon))
521 imax = int(math.floor(math.log(max / float(f)) /
522 math.log(shiftfracs.shift) + 0.5 * self.epsilon))
523 for i in range(imin, imax + 1):
524 pos = f * frac(shiftfracs.shift, 1, i)
525 fracticks.append(tick(pos.enum, pos.denom, ticklevel = ticklevel, labellevel = labellevel))
526 ticks = _mergeticklists(ticks, fracticks)
527 return ticks
530 class autologpart(logpart):
532 defaultlist = (((logpart.shiftfracs1, # ticks
533 logpart.shiftfracs1to9), # subticks
534 (logpart.shiftfracs1, # labels
535 logpart.shiftfracs125)), # sublevels
537 ((logpart.shiftfracs1, # ticks
538 logpart.shiftfracs1to9), # subticks
539 None), # labels like ticks
541 ((logpart.shift2fracs1, # ticks
542 logpart.shiftfracs1), # subticks
543 None), # labels like ticks
545 ((logpart.shift3fracs1, # ticks
546 logpart.shiftfracs1), # subticks
547 None), # labels like ticks
549 ((logpart.shift4fracs1, # ticks
550 logpart.shiftfracs1), # subticks
551 None), # labels like ticks
553 ((logpart.shift5fracs1, # ticks
554 logpart.shiftfracs1), # subticks
555 None)) # labels like ticks
557 def __init__(self, list=defaultlist, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
558 self.multipart = 1
559 self.list = list
560 if len(list) > 2:
561 self.listindex = divmod(len(list), 2)[0]
562 else:
563 self.listindex = 0
564 self.extendtick = extendtick
565 self.extendlabel = extendlabel
566 self.epsilon = epsilon
567 self.mix = mix
569 def defaultpart(self, min, max, extendmin, extendmax):
570 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
571 self.morelistindex = self.listindex
572 self.lesslistindex = self.listindex
573 part = logpart(ticks=self.list[self.listindex][0], labels=self.list[self.listindex][1],
574 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
575 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
577 def lesspart(self):
578 self.lesslistindex += 1
579 if self.lesslistindex < len(self.list):
580 part = logpart(ticks=self.list[self.lesslistindex][0], labels=self.list[self.lesslistindex][1],
581 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
582 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
583 return None
585 def morepart(self):
586 self.morelistindex -= 1
587 if self.morelistindex >= 0:
588 part = logpart(ticks=self.list[self.morelistindex][0], labels=self.list[self.morelistindex][1],
589 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
590 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
591 return None
595 ################################################################################
596 # rate partitions
597 ################################################################################
600 class cuberate:
602 def __init__(self, opt, left=None, right=None, weight=1):
603 if left is None:
604 left = 0
605 if right is None:
606 right = 3*opt
607 self.opt = opt
608 self.left = left
609 self.right = right
610 self.weight = weight
612 def rate(self, value, dense=1):
613 opt = self.opt * dense
614 if value < opt:
615 other = self.left * dense
616 elif value > opt:
617 other = self.right * dense
618 else:
619 return 0
620 factor = (value - opt) / float(other - opt)
621 return self.weight * (factor ** 3)
624 class distancerate:
626 def __init__(self, opt, weight=0.1):
627 self.opt_str = opt
628 self.weight = weight
630 def _rate(self, distances, dense=1):
631 if len(distances):
632 opt = unit.topt(unit.length(self.opt_str, default_type="v")) / dense
633 rate = 0
634 for distance in distances:
635 if distance < opt:
636 rate += self.weight * (opt / distance - 1)
637 else:
638 rate += self.weight * (distance / opt - 1)
639 return rate / float(len(distances))
642 class axisrater:
644 linticks = (cuberate(4), cuberate(10, weight=0.5), )
645 linlabels = (cuberate(4), )
646 logticks = (cuberate(5, right=20), cuberate(20, right=100, weight=0.5), )
647 loglabels = (cuberate(5, right=20), cuberate(5, left=-20, right=20, weight=0.5), )
648 stdtickrange = cuberate(1, weight=2)
649 stddistance = distancerate("1 cm")
651 def __init__(self, ticks=linticks, labels=linlabels, tickrange=stdtickrange, distance=stddistance):
652 self.ticks = ticks
653 self.labels = labels
654 self.tickrange = tickrange
655 self.distance = distance
657 def ratepart(self, axis, part, dense=1):
658 tickslen = len(self.ticks)
659 labelslen = len(self.labels)
660 ticks = [0]*tickslen
661 labels = [0]*labelslen
662 if part is not None:
663 for tick in part:
664 if tick.ticklevel is not None:
665 for level in xrange(tick.ticklevel, tickslen):
666 ticks[level] += 1
667 if tick.labellevel is not None:
668 for level in xrange(tick.labellevel, labelslen):
669 labels[level] += 1
670 rate = 0
671 weight = 0
672 for tick, rater in zip(ticks, self.ticks):
673 rate += rater.rate(tick, dense=dense)
674 weight += rater.weight
675 for label, rater in zip(labels, self.labels):
676 rate += rater.rate(label, dense=dense)
677 weight += rater.weight
678 if part is not None and len(part):
679 tickmin, tickmax = axis.gettickrange() # tickrange was not yet applied!?
680 rate += self.tickrange.rate((float(part[-1]) - float(part[0])) * axis.divisor / (tickmax - tickmin))
681 else:
682 rate += self.tickrange.rate(0)
683 weight += self.tickrange.weight
684 return rate/weight
686 def _ratedistances(self, distances, dense=1):
687 return self.distance._rate(distances, dense=dense)
690 ################################################################################
691 # box alignment, connections, distances ...
692 # (we may create a box drawing module and move all this stuff there)
693 ################################################################################
696 class BoxCrossError(Exception): pass
698 class _alignbox:
700 def __init__(self, *points):
701 self.points = len(points) - 1
702 self.x, self.y = zip(*points)
704 def path(self, centerradius = "0.05 v cm"):
705 r = unit.topt(unit.length(centerradius, default_type="v"))
706 pathels = [path._arc(self.x[0], self.y[0], r, 0, 360), path.closepath(), path._moveto(self.x[1], self.y[1])]
707 for x, y in zip(self.x, self.y)[2:]:
708 pathels.append(path._lineto(x, y))
709 pathels.append(path.closepath())
710 return path.path(*pathels)
712 def transform(self, trafo):
713 self.x, self.y = zip(*map(lambda i, trafo=trafo, x=self.x, y=self.y:
714 trafo._apply(x[i], y[i]), range(self.points + 1)))
715 return self
717 def successivepoints(self):
718 return map(lambda i, max = self.points: i and (i, i + 1) or (max, 1), range(self.points))
720 def _circlealignlinevector(self, a, dx, dy, ex, ey, fx, fy, epsilon=1e-10):
721 cx, cy = self.x[0], self.y[0]
722 gx, gy = ex - fx, ey - fy # direction vector
723 if gx*gx + gy*gy < epsilon: # zero line length
724 return None # no solution -> return None
725 rsplit = (dx*gx + dy*gy) * 1.0 / (gx*gx + gy*gy)
726 bx, by = dx - gx * rsplit, dy - gy * rsplit
727 if bx*bx + by*by < epsilon: # zero projection
728 return None # no solution -> return None
729 if bx*gy - by*gx < 0: # half space
730 return None # no solution -> return None
731 sfactor = math.sqrt((dx*dx + dy*dy) / (bx*bx + by*by))
732 bx, by = a * bx * sfactor, a * by * sfactor
733 alpha = ((bx+cx-ex)*dy - (by+cy-ey)*dx) * 1.0 / (gy*dx - gx*dy)
734 if alpha > 0 - epsilon and alpha < 1 + epsilon:
735 beta = ((ex-bx-cx)*gy - (ey-by-cy)*gx) * 1.0 / (gx*dy - gy*dx)
736 return beta*dx - cx, beta*dy - cy # valid solution -> return align tuple
737 # crossing point at the line, but outside a valid range
738 if alpha < 0:
739 return 0 # crossing point outside e
740 return 1 # crossing point outside f
742 def _linealignlinevector(self, a, dx, dy, ex, ey, fx, fy, epsilon=1e-10):
743 cx, cy = self.x[0], self.y[0]
744 gx, gy = ex - fx, ey - fy # direction vector
745 if gx*gx + gy*gy < epsilon: # zero line length
746 return None # no solution -> return None
747 if gy*dx - gx*dy < -epsilon: # half space
748 return None # no solution -> return None
749 if dx*gx + dy*gy > epsilon or dx*gx + dy*gy < -epsilon:
750 if dx*gx + dy*gy < 0: # angle bigger 90 degree
751 return 0 # use point e
752 return 1 # use point f
753 # a and g are othorgonal
754 alpha = ((a*dx+cx-ex)*dy - (a*dy+cy-ey)*dx) * 1.0 / (gy*dx - gx*dy)
755 if alpha > 0 - epsilon and alpha < 1 + epsilon:
756 beta = ((ex-a*dx-cx)*gy - (ey-a*dy-cy)*gx) * 1.0 / (gx*dy - gy*dx)
757 return beta*dx - cx, beta*dy - cy # valid solution -> return align tuple
758 # crossing point at the line, but outside a valid range
759 if alpha < 0:
760 return 0 # crossing point outside e
761 return 1 # crossing point outside f
763 def _circlealignpointvector(self, a, dx, dy, px, py, epsilon=1e-10):
764 if a*a < epsilon:
765 return None
766 cx, cy = self.x[0], self.y[0]
767 p = 2 * ((px-cx)*dx + (py-cy)*dy)
768 q = ((px-cx)*(px-cx) + (py-cy)*(py-cy) - a*a)
769 if p*p/4 - q < 0:
770 return None
771 if a > 0:
772 alpha = - p / 2 + math.sqrt(p*p/4 - q)
773 else:
774 alpha = - p / 2 - math.sqrt(p*p/4 - q)
775 return alpha*dx - cx, alpha*dy - cy
777 def _linealignpointvector(self, a, dx, dy, px, py):
778 cx, cy = self.x[0], self.y[0]
779 beta = (a*dx+cx-px)*dy - (a*dy+cy-py)*dx
780 return a*dx - beta*dy - px, a*dy + beta*dx - py
782 def _alignvector(self, a, dx, dy, alignlinevector, alignpointvector):
783 linevectors = map(lambda (i, j), self=self, a=a, dx=dx, dy=dy, x=self.x, y=self.y, alignlinevector=alignlinevector:
784 alignlinevector(a, dx, dy, x[i], y[i], x[j], y[j]), self.successivepoints())
785 for linevector in linevectors:
786 if type(linevector) is types.TupleType:
787 return linevector
788 for i, j in self.successivepoints():
789 if ((linevectors[i-1] == 1 and linevectors[j-1] == 0) or
790 (linevectors[i-1] == 1 and linevectors[j-1] is None) or
791 (linevectors[i-1] is None and linevectors[j-1] == 0)):
792 k = j == 1 and self.points or j - 1
793 return alignpointvector(a, dx, dy, self.x[k], self.y[k])
794 return a*dx, a*dy
796 def _circlealignvector(self, a, dx, dy):
797 return self._alignvector(a, dx, dy, self._circlealignlinevector, self._circlealignpointvector)
799 def _linealignvector(self, a, dx, dy):
800 return self._alignvector(a, dx, dy, self._linealignlinevector, self._linealignpointvector)
802 def circlealignvector(self, a, dx, dy):
803 return map(unit.t_pt, self._circlealignvector(unit.topt(a), dx, dy))
805 def linealignvector(self, a, dx, dy):
806 return map(unit.t_pt, self._linealignvector(unit.topt(a), dx, dy))
808 def _circlealign(self, *args):
809 self.transform(trafo._translate(*self._circlealignvector(*args)))
810 return self
812 def _linealign(self, *args):
813 self.transform(trafo._translate(*self._linealignvector(*args)))
814 return self
816 def circlealign(self, *args):
817 self.transform(trafo.translate(*self.circlealignvector(*args)))
818 return self
820 def linealign(self, *args):
821 self.transform(trafo.translate(*self.linealignvector(*args)))
822 return self
824 def _extent(self, dx, dy):
825 x1, y1 = self._linealignvector(0, dx, dy)
826 x2, y2 = self._linealignvector(0, -dx, -dy)
827 return (x1-x2)*dx + (y1-y2)*dy
829 def extent(self, dx, dy):
830 return unit.t_pt(self._extent(dx, dy))
832 def _pointdistance(self, x, y):
833 result = None
834 for i, j in self.successivepoints():
835 gx, gy = self.x[j] - self.x[i], self.y[j] - self.y[i]
836 if gx * gx + gy * gy < 1e-10:
837 dx, dy = self.x[i] - x, self.y[i] - y
838 else:
839 a = (gx * (x - self.x[i]) + gy * (y - self.y[i])) / (gx * gx + gy * gy)
840 if a < 0:
841 dx, dy = self.x[i] - x, self.y[i] - y
842 elif a > 1:
843 dx, dy = self.x[j] - x, self.y[j] - y
844 else:
845 dx, dy = x - self.x[i] - a * gx, y - self.y[i] - a * gy
846 new = math.sqrt(dx * dx + dy * dy)
847 if result is None or new < result:
848 result = new
849 return result
851 def pointdistance(self, x, y):
852 return unit.t_pt(self._pointdistance(unit.topt(x), unit.topt(y)))
854 def _boxdistance(self, other, epsilon = 1e-10):
855 # XXX: boxes crossing and distance calculation is O(N^2)
856 for i, j in self.successivepoints():
857 for k, l in other.successivepoints():
858 a = (other.y[l]-other.y[k])*(other.x[k]-self.x[i]) - (other.x[l]-other.x[k])*(other.y[k]-self.y[i])
859 b = (self.y[j]-self.y[i])*(other.x[k]-self.x[i]) - (self.x[j]-self.x[i])*(other.y[k]-self.y[i])
860 c = (self.x[j]-self.x[i])*(other.y[l]-other.y[k]) - (self.y[j]-self.y[i])*(other.x[l]-other.x[k])
861 if (abs(c) > 1e-10 and
862 a / c > -epsilon and a / c < 1 + epsilon and
863 b / c > -epsilon and b / c < 1 + epsilon):
864 raise BoxCrossError
865 result = None
866 for x, y in zip(other.x, other.y)[1:]:
867 new = self._pointdistance(x, y)
868 if result is None or new < result:
869 result = new
870 for x, y in zip(self.x, self.y)[1:]:
871 new = other._pointdistance(x, y)
872 if result is None or new < result:
873 result = new
874 return result
876 def boxdistance(self, other):
877 return unit.t_pt(self._boxdistance(other))
880 class alignbox(_alignbox):
882 def __init__(self, *args):
883 args = map(unit.topt, args)
884 _alignbox.__init__(self, *args)
887 class _rectbox(_alignbox):
889 def __init__(self, llx, lly, urx, ury, x0=0, y0=0):
890 if llx > urx: llx, urx = urx, llx
891 if lly > ury: lly, ury = ury, lly
892 _alignbox.__init__(self, (x0, y0), (llx, lly), (urx, lly), (urx, ury), (llx, ury))
895 class rectbox(_rectbox):
897 def __init__(self, *arglist, **argdict):
898 arglist = map(unit.topt, arglist)
899 for key in argdict.keys():
900 argdict[key] = unit.topt(argdict[key])
901 _rectbox.__init__(self, *argslist, **argdict)
904 class textbox(_rectbox, attrlist.attrlist):
906 def __init__(self, _tex, text, textattrs = (), vtext="0"):
907 self.tex = _tex
908 self.text = text
909 self.textattrs = textattrs
910 self.reldx, self.reldy = 1, 0
911 self.halign = self.attrget(self.textattrs, tex.halign, None)
912 self.textattrs = self.attrdel(self.textattrs, tex.halign)
913 self.direction = self.attrget(self.textattrs, tex.direction, None)
914 hwdtextattrs = self.attrdel(self.textattrs, tex.direction)
915 self.ht = unit.topt(self.tex.textht(text, *hwdtextattrs))
916 self.wd = unit.topt(self.tex.textwd(text, *hwdtextattrs))
917 self.dp = unit.topt(self.tex.textdp(text, *hwdtextattrs))
918 self.shiftht = 0.5 * unit.topt(self.tex.textht(vtext, *hwdtextattrs))
919 self.manualextents()
921 def manualextents(self, ht = None, wd = None, dp = None, shiftht = None):
922 if ht is not None: self.ht = ht
923 if wd is not None: self.wd = wd
924 if dp is not None: self.dp = dp
925 if shiftht is not None: self.shiftht = None
926 self.xtext = 0
927 self.ytext = 0
928 xorigin = 0.5 * self.wd
929 if self.halign is not None:
930 # centered by default!
931 if self.halign is tex.halign.left:
932 xorigin = 0
933 if self.halign is tex.halign.right:
934 xorigin = self.wd
935 _rectbox.__init__(self, 0, -self.dp, self.wd, self.ht, xorigin, self.shiftht)
936 if self.direction is not None:
937 self.transform(trafo._rotate(self.direction.value))
939 def transform(self, trafo):
940 _rectbox.transform(self, trafo)
941 self.xtext, self.ytext = trafo._apply(self.xtext, self.ytext)
943 def printtext(self):
944 self.tex._text(self.xtext, self.ytext, self.text, *self.textattrs)
948 ################################################################################
949 # axis painter
950 ################################################################################
953 class axispainter(attrlist.attrlist):
955 defaultticklengths = ["%0.5f cm" % (0.2*goldenrule**(-i)) for i in range(10)]
957 paralleltext = -90
958 orthogonaltext = 0
960 fractypeauto = 1
961 fractyperat = 2
962 fractypedec = 3
963 fractypeexp = 4
965 def __init__(self, innerticklengths=defaultticklengths,
966 outerticklengths=None,
967 tickattrs=(),
968 gridattrs=None,
969 zerolineattrs=(),
970 baselineattrs=canvas.linecap.square,
971 labeldist="0.3 cm",
972 labelattrs=((), tex.fontsize.footnotesize),
973 labeldirection=None,
974 labelhequalize=0,
975 labelvequalize=1,
976 titledist="0.3 cm",
977 titleattrs=(),
978 titledirection=-90,
979 titlepos=0.5,
980 fractype=fractypeauto,
981 ratfracsuffixenum=1,
982 ratfracover=r"\over",
983 decfracpoint=".",
984 expfractimes=r"\cdot",
985 expfracpre1=0,
986 expfracminexp=4,
987 suffix0=0,
988 suffix1=0):
989 self.innerticklengths_str = innerticklengths
990 self.outerticklengths_str = outerticklengths
991 self.tickattrs = tickattrs
992 self.gridattrs = gridattrs
993 self.zerolineattrs = zerolineattrs
994 self.baselineattrs = baselineattrs
995 self.labeldist_str = labeldist
996 self.labelattrs = labelattrs
997 self.labeldirection = labeldirection
998 self.labelhequalize = labelhequalize
999 self.labelvequalize = labelvequalize
1000 self.titledist_str = titledist
1001 self.titleattrs = titleattrs
1002 self.titledirection = titledirection
1003 self.titlepos = titlepos
1004 self.fractype = fractype
1005 self.ratfracsuffixenum = ratfracsuffixenum
1006 self.ratfracover = ratfracover
1007 self.decfracpoint = decfracpoint
1008 self.expfractimes = expfractimes
1009 self.expfracpre1 = expfracpre1
1010 self.expfracminexp = expfracminexp
1011 self.suffix0 = suffix0
1012 self.suffix1 = suffix1
1014 def reldirection(self, direction, dx, dy, epsilon=1e-10):
1015 direction += math.atan2(dy, dx) * 180 / math.pi
1016 while (direction > 90 + epsilon):
1017 direction -= 180
1018 while (direction < -90 - epsilon):
1019 direction += 180
1020 return direction
1022 def gcd(self, m, n):
1023 # greates common divisor, m & n must be non-negative
1024 if m < n:
1025 m, n = n, m
1026 while n > 0:
1027 m, (dummy, n) = n, divmod(m, n)
1028 return m
1030 def attachsuffix(self, tick, str):
1031 if self.suffix0 or tick.enum:
1032 if tick.suffix is not None and not self.suffix1:
1033 if str == "1":
1034 str = ""
1035 elif str == "-1":
1036 str = "-"
1037 if tick.suffix is not None:
1038 str = str + tick.suffix
1039 return str
1041 def ratfrac(self, tick):
1042 m, n = tick.enum, tick.denom
1043 sign = 1
1044 if m < 0: m, sign = -m, -sign
1045 if n < 0: n, sign = -n, -sign
1046 gcd = self.gcd(m, n)
1047 (m, dummy1), (n, dummy2) = divmod(m, gcd), divmod(n, gcd)
1048 if n != 1:
1049 if self.ratfracsuffixenum:
1050 if sign == -1:
1051 return "-{{%s}%s{%s}}" % (self.attachsuffix(tick, str(m)), self.ratfracover, n)
1052 else:
1053 return "{{%s}%s{%s}}" % (self.attachsuffix(tick, str(m)), self.ratfracover, n)
1054 else:
1055 if sign == -1:
1056 return self.attachsuffix(tick, "-{{%s}%s{%s}}" % (m, self.ratfracover, n))
1057 else:
1058 return self.attachsuffix(tick, "{{%s}%s{%s}}" % (m, self.ratfracover, n))
1059 else:
1060 if sign == -1:
1061 return self.attachsuffix(tick, "-%s" % m)
1062 else:
1063 return self.attachsuffix(tick, "%s" % m)
1065 def decfrac(self, tick):
1066 m, n = tick.enum, tick.denom
1067 sign = 1
1068 if m < 0: m, sign = -m, -sign
1069 if n < 0: n, sign = -n, -sign
1070 gcd = self.gcd(m, n)
1071 (m, dummy1), (n, dummy2) = divmod(m, gcd), divmod(n, gcd)
1072 frac, rest = divmod(m, n)
1073 strfrac = str(frac)
1074 rest = m % n
1075 if rest:
1076 strfrac += self.decfracpoint
1077 oldrest = []
1078 while (rest):
1079 if rest in oldrest:
1080 periodstart = len(strfrac) - (len(oldrest) - oldrest.index(rest))
1081 strfrac = strfrac[:periodstart] + r"\overline{" + strfrac[periodstart:] + "}"
1082 break
1083 oldrest += [rest]
1084 rest *= 10
1085 frac, rest = divmod(rest, n)
1086 strfrac += str(frac)
1087 if sign == -1:
1088 return self.attachsuffix(tick, "-%s" % strfrac)
1089 else:
1090 return self.attachsuffix(tick, strfrac)
1092 def expfrac(self, tick, minexp = None):
1093 m, n = tick.enum, tick.denom
1094 sign = 1
1095 if m < 0: m, sign = -m, -sign
1096 if n < 0: n, sign = -n, -sign
1097 exp = 0
1098 if m:
1099 while divmod(m, n)[0] > 9:
1100 n *= 10
1101 exp += 1
1102 while divmod(m, n)[0] < 1:
1103 m *= 10
1104 exp -= 1
1105 if minexp is not None and ((exp < 0 and -exp < minexp) or (exp >= 0 and exp < minexp)):
1106 return None
1107 dummy = frac(m, n)
1108 dummy.suffix = None
1109 prefactor = self.decfrac(dummy)
1110 if prefactor == "1" and not self.expfracpre1:
1111 if sign == -1:
1112 return self.attachsuffix(tick, "-10^{%i}" % exp)
1113 else:
1114 return self.attachsuffix(tick, "10^{%i}" % exp)
1115 else:
1116 if sign == -1:
1117 return self.attachsuffix(tick, "-%s%s10^{%i}" % (prefactor, self.expfractimes, exp))
1118 else:
1119 return self.attachsuffix(tick, "%s%s10^{%i}" % (prefactor, self.expfractimes, exp))
1121 def createtext(self, tick):
1122 if self.fractype == axispainter.fractypeauto:
1123 if tick.suffix is not None:
1124 tick.text = self.ratfrac(tick)
1125 else:
1126 tick.text = self.expfrac(tick, self.expfracminexp)
1127 if tick.text is None:
1128 tick.text = self.decfrac(tick)
1129 elif self.fractype == axispainter.fractypedec:
1130 tick.text = self.decfrac(tick)
1131 elif self.fractype == axispainter.fractypeexp:
1132 tick.text = self.expfrac(tick)
1133 elif self.fractype == axispainter.fractyperat:
1134 tick.text = self.ratfrac(tick)
1135 else:
1136 raise ValueError("fractype invalid")
1137 if not self.attrcount(tick.labelattrs, tex.style):
1138 tick.labelattrs += [tex.style.math]
1140 def dolayout(self, graph, axis):
1141 labeldist = unit.topt(unit.length(self.labeldist_str, default_type="v"))
1142 for tick in axis.ticks:
1143 tick.virtual = axis.convert(float(tick) * axis.divisor)
1144 tick.x, tick.y = axis._vtickpoint(axis, tick.virtual)
1145 tick.dx, tick.dy = axis.vtickdirection(axis, tick.virtual)
1146 for tick in axis.ticks:
1147 if tick.labellevel is not None:
1148 tick.labelattrs = _getsequenceno(self.labelattrs, tick.labellevel)
1149 if tick.labelattrs is not None:
1150 tick.labelattrs = list(_ensuresequence(tick.labelattrs))
1151 if tick.text is None:
1152 tick.suffix = axis.suffix
1153 self.createtext(tick)
1154 if self.labeldirection is not None and not self.attrcount(tick.labelattrs, tex.direction):
1155 tick.labelattrs += [tex.direction(self.reldirection(self.labeldirection, tick.dx, tick.dy))]
1156 tick.textbox = textbox(graph.tex, tick.text, textattrs=tick.labelattrs)
1157 else:
1158 tick.textbox = None
1159 else:
1160 tick.textbox = None
1161 for tick in axis.ticks[1:]:
1162 if tick.dx != axis.ticks[0].dx or tick.dy != axis.ticks[0].dy:
1163 equaldirection = 0
1164 break
1165 else:
1166 equaldirection = 1
1167 if equaldirection:
1168 maxht, maxwd, maxdp = 0, 0, 0
1169 for tick in axis.ticks:
1170 if tick.textbox is not None:
1171 if maxht < tick.textbox.ht: maxht = tick.textbox.ht
1172 if maxwd < tick.textbox.wd: maxwd = tick.textbox.wd
1173 if maxdp < tick.textbox.dp: maxdp = tick.textbox.dp
1174 for tick in axis.ticks:
1175 if tick.textbox is not None:
1176 if self.labelhequalize:
1177 tick.textbox.manualextents(wd = maxwd)
1178 if self.labelvequalize:
1179 tick.textbox.manualextents(ht = maxht, dp = maxdp)
1180 for tick in axis.ticks:
1181 if tick.textbox is not None:
1182 tick.textbox._linealign(labeldist, tick.dx, tick.dy)
1183 tick._extent = tick.textbox._extent(tick.dx, tick.dy) + labeldist
1184 tick.textbox.transform(trafo._translate(tick.x, tick.y))
1185 def topt_v_recursive(arg):
1186 if _issequence(arg):
1187 # return map(topt_v_recursive, arg) needs python2.2
1188 return [unit.topt(unit.length(a, default_type="v")) for a in arg]
1189 else:
1190 if arg is not None:
1191 return unit.topt(unit.length(arg, default_type="v"))
1192 innerticklengths = topt_v_recursive(self.innerticklengths_str)
1193 outerticklengths = topt_v_recursive(self.outerticklengths_str)
1194 titledist = unit.topt(unit.length(self.titledist_str, default_type="v"))
1195 axis._extent = 0
1196 for tick in axis.ticks:
1197 if tick.ticklevel is not None:
1198 tick.innerticklength = _getitemno(innerticklengths, tick.ticklevel)
1199 tick.outerticklength = _getitemno(outerticklengths, tick.ticklevel)
1200 if tick.innerticklength is not None and tick.outerticklength is None:
1201 tick.outerticklength = 0
1202 if tick.outerticklength is not None and tick.innerticklength is None:
1203 tick.innerticklength = 0
1204 if tick.textbox is None:
1205 if tick.outerticklength is not None and tick.outerticklength > 0:
1206 tick._extent = tick.outerticklength
1207 else:
1208 tick._extent = 0
1209 if axis._extent < tick._extent:
1210 axis._extent = tick._extent
1212 if axis.title is not None and self.titleattrs is not None:
1213 dx, dy = axis.vtickdirection(axis, self.titlepos)
1214 # no not modify self.titleattrs ... the painter might be used by several axes!!!
1215 titleattrs = list(_ensuresequence(self.titleattrs))
1216 if self.titledirection is not None and not self.attrcount(titleattrs, tex.direction):
1217 titleattrs = titleattrs + [tex.direction(self.reldirection(self.titledirection, dx, dy))]
1218 axis.titlebox = textbox(graph.tex, axis.title, textattrs=titleattrs)
1219 axis._extent += titledist
1220 axis.titlebox._linealign(axis._extent, dx, dy)
1221 axis.titlebox.transform(trafo._translate(*axis._vtickpoint(axis, self.titlepos)))
1222 axis._extent += axis.titlebox._extent(dx, dy)
1224 def ratelayout(self, graph, axis, dense=1):
1225 ticktextboxes = [tick.textbox for tick in axis.ticks if tick.textbox is not None]
1226 if len(ticktextboxes) > 1:
1227 try:
1228 distances = [ticktextboxes[i]._boxdistance(ticktextboxes[i+1]) for i in range(len(ticktextboxes) - 1)]
1229 except BoxCrossError:
1230 return None
1231 rate = axis.rate._ratedistances(distances, dense)
1232 return rate
1234 def paint(self, graph, axis):
1235 for tick in axis.ticks:
1236 if tick.ticklevel is not None:
1237 if tick != frac(0, 1) or self.zerolineattrs is None:
1238 gridattrs = _getsequenceno(self.gridattrs, tick.ticklevel)
1239 if gridattrs is not None:
1240 graph.stroke(axis.vgridpath(tick.virtual), *_ensuresequence(gridattrs))
1241 tickattrs = _getsequenceno(self.tickattrs, tick.ticklevel)
1242 if None not in (tick.innerticklength, tick.outerticklength, tickattrs):
1243 x1 = tick.x - tick.dx * tick.innerticklength
1244 y1 = tick.y - tick.dy * tick.innerticklength
1245 x2 = tick.x + tick.dx * tick.outerticklength
1246 y2 = tick.y + tick.dy * tick.outerticklength
1247 graph.stroke(path._line(x1, y1, x2, y2), *_ensuresequence(tickattrs))
1248 if tick.textbox is not None:
1249 tick.textbox.printtext()
1250 if self.baselineattrs is not None:
1251 graph.stroke(axis.vbaseline(axis), *_ensuresequence(self.baselineattrs))
1252 if self.zerolineattrs is not None:
1253 if len(axis.ticks) and axis.ticks[0] * axis.ticks[-1] < frac(0, 1):
1254 graph.stroke(axis.vgridpath(axis.convert(0)), *_ensuresequence(self.zerolineattrs))
1255 if axis.title is not None and self.titleattrs is not None:
1256 axis.titlebox.printtext()
1258 class splitaxispainter(attrlist.attrlist): # XXX: avoid code duplication with axispainter via inheritance
1260 paralleltext = -90
1261 orthogonaltext = 0
1263 def __init__(self, breaklinesdist=0.05,
1264 breaklineslength=0.5,
1265 breaklinesangle=-60,
1266 breaklinesattrs=(),
1267 titledist="0.3 cm",
1268 titleattrs=(),
1269 titledirection=-90,
1270 titlepos=0.5):
1271 self.breaklinesdist_str = breaklinesdist
1272 self.breaklineslength_str = breaklineslength
1273 self.breaklinesangle = breaklinesangle
1274 self.breaklinesattrs = breaklinesattrs
1275 self.titledist_str = titledist
1276 self.titleattrs = titleattrs
1277 self.titledirection = titledirection
1278 self.titlepos = titlepos
1280 def reldirection(self, direction, dx, dy, epsilon=1e-10):
1281 # XXX: direct code duplication from axispainter
1282 direction += math.atan2(dy, dx) * 180 / math.pi
1283 while (direction > 90 + epsilon):
1284 direction -= 180
1285 while (direction < -90 - epsilon):
1286 direction += 180
1287 return direction
1289 def subvbaseline(self, axis, v1=None, v2=None):
1290 if v1 is None:
1291 if self.breaklinesattrs is None:
1292 left = axis.vmin
1293 else:
1294 if axis.vminover is None:
1295 left = None
1296 else:
1297 left = axis.vminover
1298 else:
1299 left = axis.vmin+v1*(axis.vmax-axis.vmin)
1300 if v2 is None:
1301 if self.breaklinesattrs is None:
1302 right = axis.vmax
1303 else:
1304 if axis.vmaxover is None:
1305 right = None
1306 else:
1307 right = axis.vmaxover
1308 else:
1309 right = axis.vmin+v2*(axis.vmax-axis.vmin)
1310 return axis.baseaxis.vbaseline(axis.baseaxis, left, right)
1312 def dolayout(self, graph, axis):
1313 if self.breaklinesattrs is not None:
1314 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1315 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1316 self._breaklinesdist = unit.topt(self.breaklinesdist)
1317 self._breaklineslength = unit.topt(self.breaklineslength)
1318 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1319 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1320 axis._extent = (math.fabs(0.5 * self._breaklinesdist * self.cos) +
1321 math.fabs(0.5 * self._breaklineslength * self.sin))
1322 else:
1323 axis._extent = 0
1324 for subaxis in axis.axislist:
1325 subaxis.baseaxis = axis
1326 subaxis._vtickpoint = lambda axis, v: axis.baseaxis._vtickpoint(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1327 subaxis.vtickdirection = lambda axis, v: axis.baseaxis.vtickdirection(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1328 subaxis.vbaseline = self.subvbaseline
1329 subaxis.dolayout(graph)
1330 if axis._extent < subaxis._extent:
1331 axis._extent = subaxis._extent
1332 titledist = unit.topt(unit.length(self.titledist_str, default_type="v"))
1333 if axis.title is not None and self.titleattrs is not None:
1334 dx, dy = axis.vtickdirection(axis, self.titlepos)
1335 titleattrs = list(_ensuresequence(self.titleattrs))
1336 if self.titledirection is not None and not self.attrcount(titleattrs, tex.direction):
1337 titleattrs = titleattrs + [tex.direction(self.reldirection(self.titledirection, dx, dy))]
1338 axis.titlebox = textbox(graph.tex, axis.title, textattrs=titleattrs)
1339 axis._extent += titledist
1340 axis.titlebox._linealign(axis._extent, dx, dy)
1341 axis.titlebox.transform(trafo._translate(*axis._vtickpoint(axis, self.titlepos)))
1342 axis._extent += axis.titlebox._extent(dx, dy)
1344 def paint(self, graph, axis):
1345 for subaxis in axis.axislist:
1346 subaxis.dopaint(graph)
1347 if self.breaklinesattrs is not None:
1348 for subaxis1, subaxis2 in zip(axis.axislist[:-1], axis.axislist[1:]):
1349 # use a tangent of the baseline (this is independend of the tickdirection)
1350 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1351 breakline = path.normpath(axis.vbaseline(axis, v, None)).tangent(0, self.breaklineslength)
1352 widthline = path.normpath(axis.vbaseline(axis, v, None)).tangent(0, self.breaklinesdist).transformed(trafo.rotate(self.breaklinesangle+90, *breakline.begin()))
1353 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1354 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1355 breakline = breakline.transformed(trafo.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1356 breakline1 = breakline.transformed(trafo.translate(*towidth))
1357 breakline2 = breakline.transformed(trafo.translate(-towidth[0], -towidth[1]))
1358 graph.fill(path.path(path.moveto(*breakline1.begin()),
1359 path.lineto(*breakline1.end()),
1360 path.lineto(*breakline2.end()),
1361 path.lineto(*breakline2.begin()),
1362 path.closepath()), color.gray.white)
1363 graph.stroke(breakline1, *_ensuresequence(self.breaklinesattrs))
1364 graph.stroke(breakline2, *_ensuresequence(self.breaklinesattrs))
1365 if axis.title is not None and self.titleattrs is not None:
1366 axis.titlebox.printtext()
1369 class baraxispainter(attrlist.attrlist): # XXX: avoid code duplication with axispainter via inheritance
1371 paralleltext = -90
1372 orthogonaltext = 0
1374 def __init__(self, innerticklength=None,
1375 outerticklength=None,
1376 tickattrs=(),
1377 baselineattrs=canvas.linecap.square,
1378 namedist="0.3 cm",
1379 nameattrs=(),
1380 namedirection=None,
1381 namepos=0.5,
1382 namehequalize=0,
1383 namevequalize=1,
1384 titledist="0.3 cm",
1385 titleattrs=(),
1386 titledirection=-90,
1387 titlepos=0.5):
1388 self.innerticklength_str = innerticklength
1389 self.outerticklength_str = outerticklength
1390 self.tickattrs = tickattrs
1391 self.baselineattrs = baselineattrs
1392 self.namedist_str = namedist
1393 self.nameattrs = nameattrs
1394 self.namedirection = namedirection
1395 self.namepos = namepos
1396 self.namehequalize = namehequalize
1397 self.namevequalize = namevequalize
1398 self.titledist_str = titledist
1399 self.titleattrs = titleattrs
1400 self.titledirection = titledirection
1401 self.titlepos = titlepos
1403 def reldirection(self, direction, dx, dy, epsilon=1e-10):
1404 # XXX: direct code duplication from axispainter
1405 direction += math.atan2(dy, dx) * 180 / math.pi
1406 while (direction > 90 + epsilon):
1407 direction -= 180
1408 while (direction < -90 - epsilon):
1409 direction += 180
1410 return direction
1412 def dolayout(self, graph, axis):
1413 axis._extent = 0
1414 if axis.multisubaxis:
1415 for name, subaxis in zip(axis.names, axis.subaxis):
1416 subaxis.vmin = axis.convert((name, 0))
1417 subaxis.vmax = axis.convert((name, 1))
1418 subaxis.baseaxis = axis
1419 subaxis._vtickpoint = lambda axis, v: axis.baseaxis._vtickpoint(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1420 subaxis.vtickdirection = lambda axis, v: axis.baseaxis.vtickdirection(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1421 subaxis.vbaseline = None
1422 subaxis.dolayout(graph)
1423 if axis._extent < subaxis._extent:
1424 axis._extent = subaxis._extent
1425 equaldirection = 1
1426 axis.namepos = []
1427 for name in axis.names:
1428 v = axis.convert((name, self.namepos))
1429 x, y = axis._vtickpoint(axis, v)
1430 dx, dy = axis.vtickdirection(axis, v)
1431 axis.namepos.append((v, x, y, dx, dy))
1432 if equaldirection and (dx != axis.namepos[0][3] or dy != axis.namepos[0][4]):
1433 equaldirection = 0
1434 axis.nameboxes = []
1435 for (v, x, y, dx, dy), name in zip(axis.namepos, axis.names):
1436 nameattrs = list(_ensuresequence(self.nameattrs))
1437 if self.namedirection is not None and not self.attrcount(nameattrs, tex.direction):
1438 nameattrs += [tex.direction(self.reldirection(self.namedirection, dx, dy))]
1439 if axis.texts.has_key(name):
1440 axis.nameboxes.append(textbox(graph.tex, str(axis.texts[name]), textattrs=nameattrs))
1441 elif axis.texts.has_key(str(name)):
1442 axis.nameboxes.append(textbox(graph.tex, str(axis.texts[str(name)]), textattrs=nameattrs))
1443 else:
1444 axis.nameboxes.append(textbox(graph.tex, str(name), textattrs=nameattrs))
1445 if equaldirection:
1446 maxht, maxwd, maxdp = 0, 0, 0
1447 for namebox in axis.nameboxes:
1448 if maxht < namebox.ht: maxht = namebox.ht
1449 if maxwd < namebox.wd: maxwd = namebox.wd
1450 if maxdp < namebox.dp: maxdp = namebox.dp
1451 for namebox in axis.nameboxes:
1452 if self.namehequalize:
1453 namebox.manualextents(wd = maxwd)
1454 if self.namevequalize:
1455 namebox.manualextents(ht = maxht, dp = maxdp)
1456 labeldist = axis._extent + unit.topt(unit.length(self.namedist_str, default_type="v"))
1457 if self.innerticklength_str is not None:
1458 axis.innerticklength = unit.topt(unit.length(self.innerticklength_str, default_type="v"))
1459 else:
1460 if self.outerticklength_str is not None:
1461 axis.innerticklength = 0
1462 else:
1463 axis.innerticklength = None
1464 if self.outerticklength_str is not None:
1465 axis.outerticklength = unit.topt(unit.length(self.outerticklength_str, default_type="v"))
1466 else:
1467 if self.innerticklength_str is not None:
1468 axis.outerticklength = 0
1469 else:
1470 axis.outerticklength = None
1471 if axis.outerticklength is not None and self.tickattrs is not None:
1472 axis._extent += axis.outerticklength
1473 for (v, x, y, dx, dy), namebox in zip(axis.namepos, axis.nameboxes):
1474 namebox._linealign(labeldist, dx, dy)
1475 namebox.transform(trafo._translate(x, y))
1476 newextent = namebox._extent(dx, dy) + labeldist
1477 if axis._extent < newextent:
1478 axis._extent = newextent
1479 titledist = unit.topt(unit.length(self.titledist_str, default_type="v"))
1480 if axis.title is not None and self.titleattrs is not None:
1481 dx, dy = axis.vtickdirection(axis, self.titlepos)
1482 titleattrs = list(_ensuresequence(self.titleattrs))
1483 if self.titledirection is not None and not self.attrcount(titleattrs, tex.direction):
1484 titleattrs = titleattrs + [tex.direction(self.reldirection(self.titledirection, dx, dy))]
1485 axis.titlebox = textbox(graph.tex, axis.title, textattrs=titleattrs)
1486 axis._extent += titledist
1487 axis.titlebox._linealign(axis._extent, dx, dy)
1488 axis.titlebox.transform(trafo._translate(*axis._vtickpoint(axis, self.titlepos)))
1489 axis._extent += axis.titlebox._extent(dx, dy)
1491 def paint(self, graph, axis):
1492 if axis.subaxis is not None:
1493 if axis.multisubaxis:
1494 for subaxis in axis.subaxis:
1495 subaxis.dopaint(graph)
1496 if None not in (self.tickattrs, axis.innerticklength, axis.outerticklength):
1497 for pos in axis.relsizes:
1498 if pos == axis.relsizes[0]:
1499 pos -= axis.firstdist
1500 elif pos != axis.relsizes[-1]:
1501 pos -= 0.5 * axis.dist
1502 v = pos / axis.relsizes[-1]
1503 x, y = axis._vtickpoint(axis, v)
1504 dx, dy = axis.vtickdirection(axis, v)
1505 x1 = x - dx * axis.innerticklength
1506 y1 = y - dy * axis.innerticklength
1507 x2 = x + dx * axis.outerticklength
1508 y2 = y + dy * axis.outerticklength
1509 graph.stroke(path._line(x1, y1, x2, y2), *_ensuresequence(self.tickattrs))
1510 if self.baselineattrs is not None:
1511 if axis.vbaseline is not None: # XXX: subbaselines (as for splitlines)
1512 graph.stroke(axis.vbaseline(axis), *_ensuresequence(self.baselineattrs))
1513 for namebox in axis.nameboxes:
1514 namebox.printtext()
1515 if axis.title is not None and self.titleattrs is not None:
1516 axis.titlebox.printtext()
1520 ################################################################################
1521 # axes
1522 ################################################################################
1524 class PartitionError(Exception): pass
1526 class _axis:
1528 def __init__(self, min=None, max=None, reverse=0, divisor=1,
1529 datavmin=None, datavmax=None, tickvmin=0, tickvmax=1,
1530 title=None, suffix=None, painter=axispainter(), dense=None):
1531 if None not in (min, max) and min > max:
1532 min, max, reverse = max, min, not reverse
1533 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
1535 self.datamin = self.datamax = self.tickmin = self.tickmax = None
1536 if datavmin is None:
1537 if self.fixmin:
1538 self.datavmin = 0
1539 else:
1540 self.datavmin = 0.05
1541 else:
1542 self.datavmin = datavmin
1543 if datavmax is None:
1544 if self.fixmax:
1545 self.datavmax = 1
1546 else:
1547 self.datavmax = 0.95
1548 else:
1549 self.datavmax = datavmax
1550 self.tickvmin = tickvmin
1551 self.tickvmax = tickvmax
1553 self.divisor = divisor
1554 self.title = title
1555 self.suffix = suffix
1556 self.painter = painter
1557 self.dense = dense
1558 self.canconvert = 0
1559 self.__setinternalrange()
1561 def __setinternalrange(self, min=None, max=None):
1562 if not self.fixmin and min is not None and (self.min is None or min < self.min):
1563 self.min = min
1564 if not self.fixmax and max is not None and (self.max is None or max > self.max):
1565 self.max = max
1566 if None not in (self.min, self.max):
1567 min, max, vmin, vmax = self.min, self.max, 0, 1
1568 self.canconvert = 1
1569 self.setbasepoints(((min, vmin), (max, vmax)))
1570 if not self.fixmin:
1571 if self.datamin is not None and self.convert(self.datamin) < self.datavmin:
1572 min, vmin = self.datamin, self.datavmin
1573 self.setbasepoints(((min, vmin), (max, vmax)))
1574 if self.tickmin is not None and self.convert(self.tickmin) < self.tickvmin:
1575 min, vmin = self.tickmin, self.tickvmin
1576 self.setbasepoints(((min, vmin), (max, vmax)))
1577 if not self.fixmax:
1578 if self.datamax is not None and self.convert(self.datamax) > self.datavmax:
1579 max, vmax = self.datamax, self.datavmax
1580 self.setbasepoints(((min, vmin), (max, vmax)))
1581 if self.tickmax is not None and self.convert(self.tickmax) > self.tickvmax:
1582 max, vmax = self.tickmax, self.tickvmax
1583 self.setbasepoints(((min, vmin), (max, vmax)))
1584 if self.reverse:
1585 self.setbasepoints(((min, vmax), (max, vmin)))
1587 def __getinternalrange(self):
1588 return self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax
1590 def __forceinternalrange(self, range):
1591 self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax = range
1592 self.__setinternalrange()
1594 def setdatarange(self, min, max):
1595 self.datamin, self.datamax = min, max
1596 self.__setinternalrange(min, max)
1598 def settickrange(self, min, max):
1599 self.tickmin, self.tickmax = min, max
1600 self.__setinternalrange(min, max)
1602 def getdatarange(self):
1603 if self.canconvert:
1604 if self.reverse:
1605 return self.invert(1-self.datavmin), self.invert(1-self.datavmax)
1606 else:
1607 return self.invert(self.datavmin), self.invert(self.datavmax)
1609 def gettickrange(self):
1610 if self.canconvert:
1611 if self.reverse:
1612 return self.invert(1-self.tickvmin), self.invert(1-self.tickvmax)
1613 else:
1614 return self.invert(self.tickvmin), self.invert(self.tickvmax)
1616 def dolayout(self, graph):
1617 if self.dense is not None:
1618 dense = self.dense
1619 else:
1620 dense = graph.dense
1621 min, max = self.gettickrange()
1622 self.ticks = self.part.defaultpart(min/self.divisor, max/self.divisor, not self.fixmin, not self.fixmax)
1623 if self.part.multipart:
1624 # lesspart and morepart can be called after defaultpart,
1625 # although some axes may share their autoparting ---
1626 # it works, because the axes are processed sequentially
1627 bestrate = self.rate.ratepart(self, self.ticks, dense)
1628 variants = [[bestrate, self.ticks]]
1629 maxworse = 2
1630 worse = 0
1631 while worse < maxworse:
1632 newticks = self.part.lesspart()
1633 if newticks is not None:
1634 newrate = self.rate.ratepart(self, newticks, dense)
1635 variants.append([newrate, newticks])
1636 if newrate < bestrate:
1637 bestrate = newrate
1638 worse = 0
1639 else:
1640 worse += 1
1641 else:
1642 worse += 1
1643 worse = 0
1644 while worse < maxworse:
1645 newticks = self.part.morepart()
1646 if newticks is not None:
1647 newrate = self.rate.ratepart(self, newticks, dense)
1648 variants.append([newrate, newticks])
1649 if newrate < bestrate:
1650 bestrate = newrate
1651 worse = 0
1652 else:
1653 worse += 1
1654 else:
1655 worse += 1
1656 variants.sort()
1657 i = 0
1658 bestrate = None
1659 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
1660 saverange = self.__getinternalrange()
1661 self.ticks = variants[i][1]
1662 if len(self.ticks):
1663 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
1664 self.painter.dolayout(graph, self)
1665 ratelayout = self.painter.ratelayout(graph, self, dense)
1666 if ratelayout is not None:
1667 variants[i][0] += ratelayout
1668 else:
1669 variants[i][0] = None
1670 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
1671 bestrate = variants[i][0]
1672 self.__forceinternalrange(saverange)
1673 i += 1
1674 if bestrate is None:
1675 raise PartitionError("no valid axis partitioning found")
1676 variants = [variant for variant in variants[:i] if variant[0] is not None]
1677 variants.sort()
1678 self.ticks = variants[0][1]
1679 if len(self.ticks):
1680 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
1681 else:
1682 if len(self.ticks):
1683 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
1684 self.painter.dolayout(graph, self)
1686 def dopaint(self, graph):
1687 self.painter.paint(graph, self)
1689 def createlinkaxis(self, **args):
1690 return linkaxis(self, **args)
1693 class linaxis(_axis, _linmap):
1695 def __init__(self, part=autolinpart(), rate=axisrater(), **args):
1696 _axis.__init__(self, **args)
1697 if self.fixmin and self.fixmax:
1698 self.relsize = self.max - self.min
1699 self.part = part
1700 self.rate = rate
1703 class logaxis(_axis, _logmap):
1705 def __init__(self, part=autologpart(), rate=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
1706 _axis.__init__(self, **args)
1707 if self.fixmin and self.fixmax:
1708 self.relsize = math.log(self.max) - math.log(self.min)
1709 self.part = part
1710 self.rate = rate
1713 class linkaxis:
1715 def __init__(self, linkedaxis, title=None, skipticklevel=None, skiplabellevel=0, painter=axispainter(zerolineattrs=None)):
1716 self.linkedaxis = linkedaxis
1717 while isinstance(self.linkedaxis, linkaxis):
1718 self.linkedaxis = self.linkedaxis.linkedaxis
1719 self.fixmin = self.linkedaxis.fixmin
1720 self.fixmax = self.linkedaxis.fixmax
1721 if self.fixmin:
1722 self.min = self.linkedaxis.min
1723 if self.fixmax:
1724 self.max = self.linkedaxis.max
1725 self.skipticklevel = skipticklevel
1726 self.skiplabellevel = skiplabellevel
1727 self.title = title
1728 self.painter = painter
1730 def ticks(self, ticks):
1731 result = []
1732 for _tick in ticks:
1733 ticklevel = _tick.ticklevel
1734 labellevel = _tick.labellevel
1735 if self.skipticklevel is not None and ticklevel >= self.skipticklevel:
1736 ticklevel = None
1737 if self.skiplabellevel is not None and labellevel >= self.skiplabellevel:
1738 labellevel = None
1739 if ticklevel is not None or labellevel is not None:
1740 result.append(tick(_tick.enum, _tick.denom, ticklevel, labellevel))
1741 return result
1742 # XXX: don't forget to calculate new text positions as soon as this is moved
1743 # outside of the paint method (when rating is moved into the axispainter)
1745 def getdatarange(self):
1746 return self.linkedaxis.getdatarange()
1748 def setdatarange(self, min, max):
1749 prevrange = self.linkedaxis.getdatarange()
1750 self.linkedaxis.setdatarange(min, max)
1751 if hasattr(self.linkedaxis, "ticks") and prevrange != self.linkedaxis.getdatarange():
1752 raise RuntimeError("linkaxis datarange setting performed while linked axis layout already fixed")
1754 def dolayout(self, graph):
1755 self.ticks = self.ticks(self.linkedaxis.ticks)
1756 self.convert = self.linkedaxis.convert
1757 self.divisor = self.linkedaxis.divisor
1758 self.suffix = self.linkedaxis.suffix
1759 self.painter.dolayout(graph, self)
1761 def dopaint(self, graph):
1762 self.painter.paint(graph, self)
1764 def createlinkaxis(self, **args):
1765 return linkaxis(self.linkedaxis)
1768 class splitaxis:
1770 def __init__(self, axislist, splitlist=0.5, splitdist=0.1, relsizesplitdist=1, title=None, painter=splitaxispainter()):
1771 self.title = title
1772 self.axislist = axislist
1773 self.painter = painter
1774 self.splitlist = list(_ensuresequence(splitlist))
1775 self.splitlist.sort()
1776 if len(self.axislist) != len(self.splitlist) + 1:
1777 for subaxis in self.axislist:
1778 if not isinstance(subaxis, linkaxis):
1779 raise ValueError("axislist and splitlist lengths do not fit together")
1780 for subaxis in self.axislist:
1781 if isinstance(subaxis, linkaxis):
1782 subaxis.vmin = subaxis.linkedaxis.vmin
1783 subaxis.vminover = subaxis.linkedaxis.vminover
1784 subaxis.vmax = subaxis.linkedaxis.vmax
1785 subaxis.vmaxover = subaxis.linkedaxis.vmaxover
1786 else:
1787 subaxis.vmin = None
1788 subaxis.vmax = None
1789 self.axislist[0].vmin = 0
1790 self.axislist[0].vminover = None
1791 self.axislist[-1].vmax = 1
1792 self.axislist[-1].vmaxover = None
1793 for i in xrange(len(self.splitlist)):
1794 if self.splitlist[i] is not None:
1795 self.axislist[i].vmax = self.splitlist[i] - 0.5*splitdist
1796 self.axislist[i].vmaxover = self.splitlist[i]
1797 self.axislist[i+1].vmin = self.splitlist[i] + 0.5*splitdist
1798 self.axislist[i+1].vminover = self.splitlist[i]
1799 i = 0
1800 while i < len(self.axislist):
1801 if self.axislist[i].vmax is None:
1802 j = relsize = relsize2 = 0
1803 while self.axislist[i + j].vmax is None:
1804 relsize += self.axislist[i + j].relsize + relsizesplitdist
1805 j += 1
1806 relsize += self.axislist[i + j].relsize
1807 vleft = self.axislist[i].vmin
1808 vright = self.axislist[i + j].vmax
1809 for k in range(i, i + j):
1810 relsize2 += self.axislist[k].relsize
1811 self.axislist[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
1812 relsize2 += 0.5 * relsizesplitdist
1813 self.axislist[k].vmaxover = self.axislist[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
1814 relsize2 += 0.5 * relsizesplitdist
1815 self.axislist[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
1816 if i == 0 and i + j + 1 == len(self.axislist):
1817 self.relsize = relsize
1818 i += j + 1
1819 else:
1820 i += 1
1822 self.fixmin = self.axislist[0].fixmin
1823 if self.fixmin:
1824 self.min = self.axislist[0].min
1825 self.fixmax = self.axislist[-1].fixmax
1826 if self.fixmax:
1827 self.max = self.axislist[-1].max
1828 self.divisor = 1
1829 self.suffix = ""
1831 def getdatarange(self):
1832 min = self.axislist[0].getdatarange()
1833 max = self.axislist[-1].getdatarange()
1834 try:
1835 return min[0], max[1]
1836 except TypeError:
1837 return None
1839 def setdatarange(self, min, max):
1840 self.axislist[0].setdatarange(min, None)
1841 self.axislist[-1].setdatarange(None, max)
1843 def gettickrange(self):
1844 min = self.axislist[0].gettickrange()
1845 max = self.axislist[-1].gettickrange()
1846 try:
1847 return min[0], max[1]
1848 except TypeError:
1849 return None
1851 def settickrange(self, min, max):
1852 self.axislist[0].settickrange(min, None)
1853 self.axislist[-1].settickrange(None, max)
1855 def convert(self, value):
1856 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
1857 if value < self.axislist[0].max:
1858 return self.axislist[0].vmin + self.axislist[0].convert(value)*(self.axislist[0].vmax-self.axislist[0].vmin)
1859 for axis in self.axislist[1:-1]:
1860 if value > axis.min and value < axis.max:
1861 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
1862 if value > self.axislist[-1].min:
1863 return self.axislist[-1].vmin + self.axislist[-1].convert(value)*(self.axislist[-1].vmax-self.axislist[-1].vmin)
1864 raise ValueError("value couldn't be assigned to a split region")
1866 def dolayout(self, graph):
1867 self.painter.dolayout(graph, self)
1869 def dopaint(self, graph):
1870 self.painter.paint(graph, self)
1872 def createlinkaxis(self, painter=None, *args):
1873 if not len(args):
1874 return splitaxis([x.createlinkaxis() for x in self.axislist], splitlist=None)
1875 if len(args) != len(self.axislist):
1876 raise IndexError("length of the argument list doesn't fit to split number")
1877 if painter is None:
1878 painter = self.painter
1879 return splitaxis([x.createlinkaxis(**arg) for x, arg in zip(self.axislist, args)], painter=painter)
1882 class baraxis:
1884 def __init__(self, subaxis=None, multisubaxis=0, title=None, dist=0.5, firstdist=None, lastdist=None, names=None, texts={}, painter=baraxispainter()):
1885 self.dist = dist
1886 if firstdist is not None:
1887 self.firstdist = firstdist
1888 else:
1889 self.firstdist = 0.5 * dist
1890 if lastdist is not None:
1891 self.lastdist = lastdist
1892 else:
1893 self.lastdist = 0.5 * dist
1894 self.relsizes = None
1895 self.fixnames = 0
1896 self.names = []
1897 for name in _ensuresequence(names):
1898 self.setname(name)
1899 self.fixnames = names is not None
1900 self.multisubaxis = multisubaxis
1901 if self.multisubaxis:
1902 self.createsubaxis = subaxis
1903 self.subaxis = [self.createsubaxis.createsubaxis() for name in self.names]
1904 else:
1905 self.subaxis = subaxis
1906 self.title = title
1907 self.fixnames = 0
1908 self.texts = texts
1909 self.painter = painter
1911 def getdatarange(self):
1912 return None
1914 def setname(self, name, *subnames):
1915 # setting self.relsizes to None forces later recalculation
1916 if not self.fixnames:
1917 if name not in self.names:
1918 self.relsizes = None
1919 self.names.append(name)
1920 if self.multisubaxis:
1921 self.subaxis.append(self.createsubaxis.createsubaxis())
1922 if (not self.fixnames or name in self.names) and len(subnames):
1923 if self.multisubaxis:
1924 if self.subaxis[self.names.index(name)].setname(*subnames):
1925 self.relsizes = None
1926 else:
1927 if self.subaxis.setname(*subnames):
1928 self.relsizes = None
1929 return self.relsizes is not None
1931 def updaterelsizes(self):
1932 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
1933 self.relsizes[-1] += self.lastdist - self.dist
1934 if self.multisubaxis:
1935 subrelsize = 0
1936 for i in range(1, len(self.relsizes)):
1937 self.subaxis[i-1].updaterelsizes()
1938 subrelsize += self.subaxis[i-1].relsizes[-1]
1939 self.relsizes[i] += subrelsize
1940 else:
1941 if self.subaxis is None:
1942 subrelsize = 1
1943 else:
1944 self.subaxis.updaterelsizes()
1945 subrelsize = self.subaxis.relsizes[-1]
1946 for i in range(1, len(self.relsizes)):
1947 self.relsizes[i] += i * subrelsize
1949 def convert(self, value):
1950 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
1951 if not self.relsizes:
1952 self.updaterelsizes()
1953 pos = self.names.index(value[0])
1954 if len(value) == 2:
1955 if self.subaxis is None:
1956 subvalue = value[1]
1957 else:
1958 if self.multisubaxis:
1959 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
1960 else:
1961 subvalue = value[1] * self.subaxis.relsizes[-1]
1962 else:
1963 if self.multisubaxis:
1964 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
1965 else:
1966 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
1967 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
1969 def dolayout(self, graph):
1970 self.painter.dolayout(graph, self)
1972 def dopaint(self, graph):
1973 self.painter.paint(graph, self)
1975 def createlinkaxis(self, **args):
1976 if self.subaxis is not None:
1977 if self.multisubaxis:
1978 subaxis = [subaxis.createlinkaxis() for subaxis in self.subaxis]
1979 else:
1980 subaxis = self.subaxis.createlinkaxis()
1981 else:
1982 subaxis = None
1983 return baraxis(subaxis=subaxis, dist=self.dist, firstdist=self.firstdist, lastdist=self.lastdist, **args)
1985 createsubaxis = createlinkaxis
1988 ################################################################################
1989 # graph
1990 ################################################################################
1993 class graphxy(canvas.canvas):
1995 Names = "x", "y"
1997 def clipcanvas(self):
1998 return self.insert(canvas.canvas(canvas.clip(path._rect(self._xpos, self._ypos, self._width, self._height))))
2000 def plot(self, data, style=None):
2001 if self.haslayout:
2002 raise RuntimeError("layout setup was already performed")
2003 if style is None:
2004 if self.defaultstyle.has_key(data.defaultstyle):
2005 style = self.defaultstyle[data.defaultstyle].iterate()
2006 else:
2007 style = data.defaultstyle()
2008 self.defaultstyle[data.defaultstyle] = style
2009 styles = []
2010 first = 1
2011 for d in _ensuresequence(data):
2012 if first:
2013 styles.append(style)
2014 else:
2015 styles.append(style.iterate())
2016 first = 0
2017 if d is not None:
2018 d.setstyle(self, styles[-1])
2019 self.data.append(d)
2020 if _issequence(data):
2021 return styles
2022 return styles[0]
2024 def _vxtickpoint(self, axis, v):
2025 return (self._xpos+v*self._width, axis.axispos)
2027 def _vytickpoint(self, axis, v):
2028 return (axis.axispos, self._ypos+v*self._height)
2030 def vtickdirection(self, axis, v):
2031 return axis.fixtickdirection
2033 def _pos(self, x, y, xaxis=None, yaxis=None):
2034 if xaxis is None: xaxis = self.axes["x"]
2035 if yaxis is None: yaxis = self.axes["y"]
2036 return self._xpos+xaxis.convert(x)*self._width, self._ypos+yaxis.convert(y)*self._height
2038 def pos(self, x, y, xaxis=None, yaxis=None):
2039 if xaxis is None: xaxis = self.axes["x"]
2040 if yaxis is None: yaxis = self.axes["y"]
2041 return self.xpos+xaxis.convert(x)*self.width, self.ypos+yaxis.convert(y)*self.height
2043 def _vpos(self, vx, vy):
2044 return self._xpos+vx*self._width, self._ypos+vy*self._height
2046 def vpos(self, vx, vy):
2047 return self.xpos+vx*self.width, self.ypos+vy*self.height
2049 def xbaseline(self, axis, x1, x2, shift=0, xaxis=None):
2050 if xaxis is None: xaxis = self.axes["x"]
2051 v1, v2 = xaxis.convert(x1), xaxis.convert(x2)
2052 return path._line(self._xpos+v1*self._width, axis.axispos+shift,
2053 self._xpos+v2*self._width, axis.axispos+shift)
2055 def ybaseline(self, axis, y1, y2, shift=0, yaxis=None):
2056 if yaxis is None: yaxis = self.axes["y"]
2057 v1, v2 = yaxis.convert(y1), yaxis.convert(y2)
2058 return path._line(axis.axispos+shift, self._ypos+v1*self._height,
2059 axis.axispos+shift, self._ypos+v2*self._height)
2061 def vxbaseline(self, axis, v1=None, v2=None, shift=0):
2062 if v1 is None: v1 = 0
2063 if v2 is None: v2 = 1
2064 return path._line(self._xpos+v1*self._width, axis.axispos+shift,
2065 self._xpos+v2*self._width, axis.axispos+shift)
2067 def vybaseline(self, axis, v1=None, v2=None, shift=0):
2068 if v1 is None: v1 = 0
2069 if v2 is None: v2 = 1
2070 return path._line(axis.axispos+shift, self._ypos+v1*self._height,
2071 axis.axispos+shift, self._ypos+v2*self._height)
2073 def xgridpath(self, x, xaxis=None):
2074 if xaxis is None: xaxis = self.axes["x"]
2075 v = xaxis.convert(x)
2076 return path._line(self._xpos+v*self._width, self._ypos,
2077 self._xpos+v*self._width, self._ypos+self._height)
2079 def ygridpath(self, y, yaxis=None):
2080 if yaxis is None: yaxis = self.axes["y"]
2081 v = yaxis.convert(y)
2082 return path._line(self._xpos, self._ypos+v*self._height,
2083 self._xpos+self._width, self._ypos+v*self._height)
2085 def vxgridpath(self, v):
2086 return path._line(self._xpos+v*self._width, self._ypos,
2087 self._xpos+v*self._width, self._ypos+self._height)
2089 def vygridpath(self, v):
2090 return path._line(self._xpos, self._ypos+v*self._height,
2091 self._xpos+self._width, self._ypos+v*self._height)
2093 def _addpos(self, x, y, dx, dy):
2094 return x+dx, y+dy
2096 def _connect(self, x1, y1, x2, y2):
2097 return path._lineto(x2, y2)
2099 def keynum(self, key):
2100 try:
2101 while key[0] in string.letters:
2102 key = key[1:]
2103 return int(key)
2104 except IndexError:
2105 return 1
2107 def gatherranges(self):
2108 ranges = {}
2109 for data in self.data:
2110 pdranges = data.getranges()
2111 if pdranges is not None:
2112 for key in pdranges.keys():
2113 if key not in ranges.keys():
2114 ranges[key] = pdranges[key]
2115 else:
2116 ranges[key] = (min(ranges[key][0], pdranges[key][0]),
2117 max(ranges[key][1], pdranges[key][1]))
2118 # known ranges are also set as ranges for the axes
2119 for key, axis in self.axes.items():
2120 if key in ranges.keys():
2121 axis.setdatarange(*ranges[key])
2122 ranges[key] = axis.getdatarange()
2123 if ranges[key] is None:
2124 del ranges[key]
2125 return ranges
2127 def removedomethod(self, method):
2128 hadmethod = 0
2129 while 1:
2130 try:
2131 self.domethods.remove(method)
2132 hadmethod = 1
2133 except ValueError:
2134 return hadmethod
2136 def dolayout(self):
2137 if not self.removedomethod(self.dolayout): return
2138 self.haslayout = 1
2139 # create list of ranges
2140 # 1. gather ranges
2141 ranges = self.gatherranges()
2142 # 2. calculate additional ranges out of known ranges
2143 for data in self.data:
2144 data.setranges(ranges)
2145 # 3. gather ranges again
2146 self.gatherranges()
2148 # do the layout for all axes
2149 axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
2150 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
2151 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
2152 self._xaxisextents = [0, 0]
2153 self._yaxisextents = [0, 0]
2154 needxaxisdist = [0, 0]
2155 needyaxisdist = [0, 0]
2156 items = list(self.axes.items())
2157 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
2158 for key, axis in items:
2159 num = self.keynum(key)
2160 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
2161 num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
2162 if XPattern.match(key):
2163 if needxaxisdist[num2]:
2164 self._xaxisextents[num2] += axesdist
2165 axis.axispos = self._ypos+num2*self._height + num3*self._xaxisextents[num2]
2166 axis._vtickpoint = self._vxtickpoint
2167 axis.fixtickdirection = (0, num3)
2168 axis.vgridpath = self.vxgridpath
2169 axis.vbaseline = self.vxbaseline
2170 axis.gridpath = self.xgridpath
2171 axis.baseline = self.xbaseline
2172 elif YPattern.match(key):
2173 if needyaxisdist[num2]:
2174 self._yaxisextents[num2] += axesdist
2175 axis.axispos = self._xpos+num2*self._width + num3*self._yaxisextents[num2]
2176 axis._vtickpoint = self._vytickpoint
2177 axis.fixtickdirection = (num3, 0)
2178 axis.vgridpath = self.vygridpath
2179 axis.vbaseline = self.vybaseline
2180 axis.gridpath = self.ygridpath
2181 axis.baseline = self.ybaseline
2182 else:
2183 raise ValueError("Axis key '%s' not allowed" % key)
2184 axis.vtickdirection = self.vtickdirection
2185 axis.dolayout(self)
2186 if XPattern.match(key):
2187 self._xaxisextents[num2] += axis._extent
2188 needxaxisdist[num2] = 1
2189 if YPattern.match(key):
2190 self._yaxisextents[num2] += axis._extent
2191 needyaxisdist[num2] = 1
2193 def dobackground(self):
2194 self.dolayout()
2195 if not self.removedomethod(self.dobackground): return
2196 if self.backgroundattrs is not None:
2197 self.draw(path._rect(self._xpos, self._ypos, self._width, self._height),
2198 *_ensuresequence(self.backgroundattrs))
2200 def doaxes(self):
2201 self.dolayout()
2202 if not self.removedomethod(self.doaxes): return
2203 for axis in self.axes.values():
2204 axis.dopaint(self)
2206 def dodata(self):
2207 self.dolayout()
2208 if not self.removedomethod(self.dodata): return
2209 for data in self.data:
2210 data.draw(self)
2212 def finish(self):
2213 while len(self.domethods):
2214 self.domethods[0]()
2216 def initwidthheight(self, width, height, ratio):
2217 if (width is not None) and (height is None):
2218 height = (1/ratio) * width
2219 if (height is not None) and (width is None):
2220 width = ratio * height
2221 self._width = unit.topt(width)
2222 self._height = unit.topt(height)
2223 self.width = width
2224 self.height = height
2225 if self._width <= 0: raise ValueError("width < 0")
2226 if self._height <= 0: raise ValueError("height < 0")
2228 def initaxes(self, axes, addlinkaxes=0):
2229 for key in self.Names:
2230 if not axes.has_key(key):
2231 axes[key] = linaxis()
2232 elif axes[key] is None:
2233 del axes[key]
2234 if addlinkaxes:
2235 if not axes.has_key(key + "2") and axes.has_key(key):
2236 axes[key + "2"] = axes[key].createlinkaxis()
2237 elif axes[key + "2"] is None:
2238 del axes[key + "2"]
2239 self.axes = axes
2241 def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, ratio=goldenrule,
2242 backgroundattrs=None, dense=1, axesdist="0.8 cm", **axes):
2243 canvas.canvas.__init__(self)
2244 self.tex = tex
2245 self.xpos = xpos
2246 self.ypos = ypos
2247 self._xpos = unit.topt(xpos)
2248 self._ypos = unit.topt(ypos)
2249 self.initwidthheight(width, height, ratio)
2250 self.initaxes(axes, 1)
2251 self.dense = dense
2252 self.axesdist_str = axesdist
2253 self.backgroundattrs = backgroundattrs
2254 self.data = []
2255 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
2256 self.haslayout = 0
2257 self.defaultstyle = {}
2259 def bbox(self):
2260 self.dolayout()
2261 return bbox.bbox(self._xpos - self._yaxisextents[0],
2262 self._ypos - self._xaxisextents[0],
2263 self._xpos + self._width + self._yaxisextents[1],
2264 self._ypos + self._height + self._xaxisextents[1])
2266 def write(self, file):
2267 self.finish()
2268 canvas.canvas.write(self, file)
2272 # some thoughts, but deferred right now
2274 # class graphxyz(graphxy):
2276 # Names = "x", "y", "z"
2278 # def _vxtickpoint(self, axis, v):
2279 # return self._vpos(v, axis.vypos, axis.vzpos)
2281 # def _vytickpoint(self, axis, v):
2282 # return self._vpos(axis.vxpos, v, axis.vzpos)
2284 # def _vztickpoint(self, axis, v):
2285 # return self._vpos(axis.vxpos, axis.vypos, v)
2287 # def vxtickdirection(self, axis, v):
2288 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
2289 # x2, y2 = self._vpos(v, 0.5, 0)
2290 # dx, dy = x1 - x2, y1 - y2
2291 # norm = math.sqrt(dx*dx + dy*dy)
2292 # return dx/norm, dy/norm
2294 # def vytickdirection(self, axis, v):
2295 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
2296 # x2, y2 = self._vpos(0.5, v, 0)
2297 # dx, dy = x1 - x2, y1 - y2
2298 # norm = math.sqrt(dx*dx + dy*dy)
2299 # return dx/norm, dy/norm
2301 # def vztickdirection(self, axis, v):
2302 # return -1, 0
2303 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
2304 # x2, y2 = self._vpos(0.5, 0.5, v)
2305 # dx, dy = x1 - x2, y1 - y2
2306 # norm = math.sqrt(dx*dx + dy*dy)
2307 # return dx/norm, dy/norm
2309 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
2310 # if xaxis is None: xaxis = self.axes["x"]
2311 # if yaxis is None: yaxis = self.axes["y"]
2312 # if zaxis is None: zaxis = self.axes["z"]
2313 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
2315 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
2316 # if xaxis is None: xaxis = self.axes["x"]
2317 # if yaxis is None: yaxis = self.axes["y"]
2318 # if zaxis is None: zaxis = self.axes["z"]
2319 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
2321 # def _vpos(self, vx, vy, vz):
2322 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
2323 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
2324 # + self.a[2]*self.b[0]*(y-self.eye[1])
2325 # + self.a[1]*self.b[2]*(x-self.eye[0])
2326 # - self.a[2]*self.b[1]*(x-self.eye[0])
2327 # - self.a[0]*self.b[2]*(y-self.eye[1])
2328 # - self.a[1]*self.b[0]*(z-self.eye[2]))
2329 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
2330 # + self.eye[2]*self.b[0]*(y-self.eye[1])
2331 # + self.eye[1]*self.b[2]*(x-self.eye[0])
2332 # - self.eye[2]*self.b[1]*(x-self.eye[0])
2333 # - self.eye[0]*self.b[2]*(y-self.eye[1])
2334 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
2335 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
2336 # + self.a[2]*self.eye[0]*(y-self.eye[1])
2337 # + self.a[1]*self.eye[2]*(x-self.eye[0])
2338 # - self.a[2]*self.eye[1]*(x-self.eye[0])
2339 # - self.a[0]*self.eye[2]*(y-self.eye[1])
2340 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
2341 # return da/d0 + self._xpos, db/d0 + self._ypos
2343 # def vpos(self, vx, vy, vz):
2344 # tx, ty = self._vpos(vx, vy, vz)
2345 # return unit.t_pt(tx), unit.t_pt(ty)
2347 # def xbaseline(self, axis, x1, x2, shift=0, xaxis=None):
2348 # if xaxis is None: xaxis = self.axes["x"]
2349 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2), shift)
2351 # def ybaseline(self, axis, y1, y2, shift=0, yaxis=None):
2352 # if yaxis is None: yaxis = self.axes["y"]
2353 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2), shift)
2355 # def zbaseline(self, axis, z1, z2, shift=0, zaxis=None):
2356 # if zaxis is None: zaxis = self.axes["z"]
2357 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2), shift)
2359 # def vxbaseline(self, axis, v1, v2, shift=0):
2360 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
2361 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
2362 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
2363 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
2365 # def vybaseline(self, axis, v1, v2, shift=0):
2366 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
2367 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
2368 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
2369 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
2371 # def vzbaseline(self, axis, v1, v2, shift=0):
2372 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
2373 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
2374 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
2375 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
2377 # def xgridpath(self, x, xaxis=None):
2378 # assert 0
2379 # if xaxis is None: xaxis = self.axes["x"]
2380 # v = xaxis.convert(x)
2381 # return path._line(self._xpos+v*self._width, self._ypos,
2382 # self._xpos+v*self._width, self._ypos+self._height)
2384 # def ygridpath(self, y, yaxis=None):
2385 # assert 0
2386 # if yaxis is None: yaxis = self.axes["y"]
2387 # v = yaxis.convert(y)
2388 # return path._line(self._xpos, self._ypos+v*self._height,
2389 # self._xpos+self._width, self._ypos+v*self._height)
2391 # def zgridpath(self, z, zaxis=None):
2392 # assert 0
2393 # if zaxis is None: zaxis = self.axes["z"]
2394 # v = zaxis.convert(z)
2395 # return path._line(self._xpos, self._zpos+v*self._height,
2396 # self._xpos+self._width, self._zpos+v*self._height)
2398 # def vxgridpath(self, v):
2399 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
2400 # path._lineto(*self._vpos(v, 0, 1)),
2401 # path._lineto(*self._vpos(v, 1, 1)),
2402 # path._lineto(*self._vpos(v, 1, 0)),
2403 # path.closepath())
2405 # def vygridpath(self, v):
2406 # return path.path(path._moveto(*self._vpos(0, v, 0)),
2407 # path._lineto(*self._vpos(0, v, 1)),
2408 # path._lineto(*self._vpos(1, v, 1)),
2409 # path._lineto(*self._vpos(1, v, 0)),
2410 # path.closepath())
2412 # def vzgridpath(self, v):
2413 # return path.path(path._moveto(*self._vpos(0, 0, v)),
2414 # path._lineto(*self._vpos(0, 1, v)),
2415 # path._lineto(*self._vpos(1, 1, v)),
2416 # path._lineto(*self._vpos(1, 0, v)),
2417 # path.closepath())
2419 # def _addpos(self, x, y, dx, dy):
2420 # assert 0
2421 # return x+dx, y+dy
2423 # def _connect(self, x1, y1, x2, y2):
2424 # assert 0
2425 # return path._lineto(x2, y2)
2427 # def doaxes(self):
2428 # self.dolayout()
2429 # if not self.removedomethod(self.doaxes): return
2430 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
2431 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
2432 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
2433 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[2])
2434 # items = list(self.axes.items())
2435 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
2436 # for key, axis in items:
2437 # num = self.keynum(key)
2438 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
2439 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
2440 # if XPattern.match(key):
2441 # axis.vypos = 0
2442 # axis.vzpos = 0
2443 # axis._vtickpoint = self._vxtickpoint
2444 # axis.vgridpath = self.vxgridpath
2445 # axis.vbaseline = self.vxbaseline
2446 # axis.vtickdirection = self.vxtickdirection
2447 # elif YPattern.match(key):
2448 # axis.vxpos = 0
2449 # axis.vzpos = 0
2450 # axis._vtickpoint = self._vytickpoint
2451 # axis.vgridpath = self.vygridpath
2452 # axis.vbaseline = self.vybaseline
2453 # axis.vtickdirection = self.vytickdirection
2454 # elif ZPattern.match(key):
2455 # axis.vxpos = 0
2456 # axis.vypos = 0
2457 # axis._vtickpoint = self._vztickpoint
2458 # axis.vgridpath = self.vzgridpath
2459 # axis.vbaseline = self.vzbaseline
2460 # axis.vtickdirection = self.vztickdirection
2461 # else:
2462 # raise ValueError("Axis key '%s' not allowed" % key)
2463 # if axis.painter is not None:
2464 # axis.dopaint(self)
2465 # # if XPattern.match(key):
2466 # # self._xaxisextents[num2] += axis._extent
2467 # # needxaxisdist[num2] = 1
2468 # # if YPattern.match(key):
2469 # # self._yaxisextents[num2] += axis._extent
2470 # # needyaxisdist[num2] = 1
2472 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
2473 # phi=30, theta=30, distance=1,
2474 # backgroundattrs=None, axesdist="0.8 cm", **axes):
2475 # canvas.canvas.__init__(self)
2476 # self.tex = tex
2477 # self.xpos = xpos
2478 # self.ypos = ypos
2479 # self._xpos = unit.topt(xpos)
2480 # self._ypos = unit.topt(ypos)
2481 # self._width = unit.topt(width)
2482 # self._height = unit.topt(height)
2483 # self._depth = unit.topt(depth)
2484 # self.width = width
2485 # self.height = height
2486 # self.depth = depth
2487 # if self._width <= 0: raise ValueError("width < 0")
2488 # if self._height <= 0: raise ValueError("height < 0")
2489 # if self._depth <= 0: raise ValueError("height < 0")
2490 # self._distance = distance*math.sqrt(self._width*self._width+
2491 # self._height*self._height+
2492 # self._depth*self._depth)
2493 # phi *= -math.pi/180
2494 # theta *= math.pi/180
2495 # self.a = (-math.sin(phi), math.cos(phi), 0)
2496 # self.b = (-math.cos(phi)*math.sin(theta),
2497 # -math.sin(phi)*math.sin(theta),
2498 # math.cos(theta))
2499 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
2500 # self._distance*math.sin(phi)*math.cos(theta),
2501 # self._distance*math.sin(theta))
2502 # self.initaxes(axes)
2503 # self.axesdist_str = axesdist
2504 # self.backgroundattrs = backgroundattrs
2506 # self.data = []
2507 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
2508 # self.haslayout = 0
2509 # self.defaultstyle = {}
2511 # def bbox(self):
2512 # self.finish()
2513 # return bbox.bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
2516 ################################################################################
2517 # attr changers
2518 ################################################################################
2521 #class _Ichangeattr:
2522 # """attribute changer
2523 # is an iterator for attributes where an attribute
2524 # is not refered by just a number (like for a sequence),
2525 # but also by the number of attributes requested
2526 # by calls of the next method (like for an color gradient)
2527 # (you should ensure to call all needed next before the attr)
2529 # the attribute itself is implemented by overloading the _attr method"""
2531 # def attr(self):
2532 # "get an attribute"
2534 # def next(self):
2535 # "get an attribute changer for the next attribute"
2538 class _changeattr: pass
2541 class changeattr(_changeattr):
2543 def __init__(self):
2544 self.counter = 1
2546 def getattr(self):
2547 return self.attr(0)
2549 def iterate(self):
2550 newindex = self.counter
2551 self.counter += 1
2552 return refattr(self, newindex)
2555 class refattr(_changeattr):
2557 def __init__(self, ref, index):
2558 self.ref = ref
2559 self.index = index
2561 def getattr(self):
2562 return self.ref.attr(self.index)
2564 def iterate(self):
2565 return self.ref.iterate()
2568 # helper routines for a using attrs
2570 def _getattr(attr):
2571 """get attr out of a attr/changeattr"""
2572 if isinstance(attr, _changeattr):
2573 return attr.getattr()
2574 return attr
2577 def _getattrs(attrs):
2578 """get attrs out of a sequence of attr/changeattr"""
2579 if attrs is not None:
2580 result = []
2581 for attr in _ensuresequence(attrs):
2582 if isinstance(attr, _changeattr):
2583 result.append(attr.getattr())
2584 else:
2585 result.append(attr)
2586 return result
2589 def _iterateattr(attr):
2590 """perform next to a attr/changeattr"""
2591 if isinstance(attr, _changeattr):
2592 return attr.iterate()
2593 return attr
2596 def _iterateattrs(attrs):
2597 """perform next to a sequence of attr/changeattr"""
2598 if attrs is not None:
2599 result = []
2600 for attr in _ensuresequence(attrs):
2601 if isinstance(attr, _changeattr):
2602 result.append(attr.iterate())
2603 else:
2604 result.append(attr)
2605 return result
2608 class changecolor(changeattr):
2610 def __init__(self, gradient):
2611 changeattr.__init__(self)
2612 self.gradient = gradient
2614 def attr(self, index):
2615 if self.counter != 1:
2616 return self.gradient.getcolor(index/float(self.counter-1))
2617 else:
2618 return self.gradient.getcolor(0)
2621 class _changecolorgray(changecolor):
2623 def __init__(self, gradient=color.gradient.Gray):
2624 changecolor.__init__(self, gradient)
2626 _changecolorgrey = _changecolorgray
2629 class _changecolorreversegray(changecolor):
2631 def __init__(self, gradient=color.gradient.ReverseGray):
2632 changecolor.__init__(self, gradient)
2634 _changecolorreversegrey = _changecolorreversegray
2637 class _changecolorredblack(changecolor):
2639 def __init__(self, gradient=color.gradient.RedBlack):
2640 changecolor.__init__(self, gradient)
2643 class _changecolorblackred(changecolor):
2645 def __init__(self, gradient=color.gradient.BlackRed):
2646 changecolor.__init__(self, gradient)
2649 class _changecolorredwhite(changecolor):
2651 def __init__(self, gradient=color.gradient.RedWhite):
2652 changecolor.__init__(self, gradient)
2655 class _changecolorwhitered(changecolor):
2657 def __init__(self, gradient=color.gradient.WhiteRed):
2658 changecolor.__init__(self, gradient)
2661 class _changecolorgreenblack(changecolor):
2663 def __init__(self, gradient=color.gradient.GreenBlack):
2664 changecolor.__init__(self, gradient)
2667 class _changecolorblackgreen(changecolor):
2669 def __init__(self, gradient=color.gradient.BlackGreen):
2670 changecolor.__init__(self, gradient)
2673 class _changecolorgreenwhite(changecolor):
2675 def __init__(self, gradient=color.gradient.GreenWhite):
2676 changecolor.__init__(self, gradient)
2679 class _changecolorwhitegreen(changecolor):
2681 def __init__(self, gradient=color.gradient.WhiteGreen):
2682 changecolor.__init__(self, gradient)
2685 class _changecolorblueblack(changecolor):
2687 def __init__(self, gradient=color.gradient.BlueBlack):
2688 changecolor.__init__(self, gradient)
2691 class _changecolorblackblue(changecolor):
2693 def __init__(self, gradient=color.gradient.BlackBlue):
2694 changecolor.__init__(self, gradient)
2697 class _changecolorbluewhite(changecolor):
2699 def __init__(self, gradient=color.gradient.BlueWhite):
2700 changecolor.__init__(self, gradient)
2703 class _changecolorwhiteblue(changecolor):
2705 def __init__(self, gradient=color.gradient.WhiteBlue):
2706 changecolor.__init__(self, gradient)
2709 class _changecolorredgreen(changecolor):
2711 def __init__(self, gradient=color.gradient.RedGreen):
2712 changecolor.__init__(self, gradient)
2715 class _changecolorredblue(changecolor):
2717 def __init__(self, gradient=color.gradient.RedBlue):
2718 changecolor.__init__(self, gradient)
2721 class _changecolorgreenred(changecolor):
2723 def __init__(self, gradient=color.gradient.GreenRed):
2724 changecolor.__init__(self, gradient)
2727 class _changecolorgreenblue(changecolor):
2729 def __init__(self, gradient=color.gradient.GreenBlue):
2730 changecolor.__init__(self, gradient)
2733 class _changecolorbluered(changecolor):
2735 def __init__(self, gradient=color.gradient.BlueRed):
2736 changecolor.__init__(self, gradient)
2739 class _changecolorbluegreen(changecolor):
2741 def __init__(self, gradient=color.gradient.BlueGreen):
2742 changecolor.__init__(self, gradient)
2745 class _changecolorrainbow(changecolor):
2747 def __init__(self, gradient=color.gradient.Rainbow):
2748 changecolor.__init__(self, gradient)
2751 class _changecolorreverserainbow(changecolor):
2753 def __init__(self, gradient=color.gradient.ReverseRainbow):
2754 changecolor.__init__(self, gradient)
2757 class _changecolorhue(changecolor):
2759 def __init__(self, gradient=color.gradient.Hue):
2760 changecolor.__init__(self, gradient)
2763 class _changecolorreversehue(changecolor):
2765 def __init__(self, gradient=color.gradient.ReverseHue):
2766 changecolor.__init__(self, gradient)
2769 changecolor.Gray = _changecolorgray
2770 changecolor.Grey = _changecolorgrey
2771 changecolor.Reversegray = _changecolorreversegray
2772 changecolor.Reversegrey = _changecolorreversegrey
2773 changecolor.Redblack = _changecolorredblack
2774 changecolor.Blackred = _changecolorblackred
2775 changecolor.Redwhite = _changecolorredwhite
2776 changecolor.WhiteRed = _changecolorwhitered
2777 changecolor.GreenBlack = _changecolorgreenblack
2778 changecolor.BlackGreen = _changecolorblackgreen
2779 changecolor.GreenWhite = _changecolorgreenwhite
2780 changecolor.WhiteGreen = _changecolorwhitegreen
2781 changecolor.BlueBlack = _changecolorblueblack
2782 changecolor.BlackBlue = _changecolorblackblue
2783 changecolor.BlueWhite = _changecolorbluewhite
2784 changecolor.WhiteBlue = _changecolorwhiteblue
2785 changecolor.RedGreen = _changecolorredgreen
2786 changecolor.RedBlue = _changecolorredblue
2787 changecolor.GreenRed = _changecolorgreenred
2788 changecolor.GreenBlue = _changecolorgreenblue
2789 changecolor.BlueRed = _changecolorbluered
2790 changecolor.BlueGreen = _changecolorbluegreen
2791 changecolor.Rainbow = _changecolorrainbow
2792 changecolor.ReverseRainbow = _changecolorreverserainbow
2793 changecolor.Hue = _changecolorhue
2794 changecolor.ReverseHue = _changecolorreversehue
2797 class changesequence(changeattr):
2798 """cycles through a sequence"""
2800 def __init__(self, *sequence):
2801 changeattr.__init__(self)
2802 if not len(sequence):
2803 sequence = self.defaultsequence
2804 self.sequence = sequence
2806 def attr(self, index):
2807 return self.sequence[index % len(self.sequence)]
2810 class changelinestyle(changesequence):
2811 defaultsequence = (canvas.linestyle.solid,
2812 canvas.linestyle.dashed,
2813 canvas.linestyle.dotted,
2814 canvas.linestyle.dashdotted)
2817 class changestrokedfilled(changesequence):
2818 defaultsequence = (canvas.stroked(), canvas.filled())
2821 class changefilledstroked(changesequence):
2822 defaultsequence = (canvas.filled(), canvas.stroked())
2826 ################################################################################
2827 # styles
2828 ################################################################################
2831 class symbol:
2833 def cross(self, x, y):
2834 return (path._moveto(x-0.5*self._size, y-0.5*self._size),
2835 path._lineto(x+0.5*self._size, y+0.5*self._size),
2836 path._moveto(x-0.5*self._size, y+0.5*self._size),
2837 path._lineto(x+0.5*self._size, y-0.5*self._size))
2839 def plus(self, x, y):
2840 return (path._moveto(x-0.707106781*self._size, y),
2841 path._lineto(x+0.707106781*self._size, y),
2842 path._moveto(x, y-0.707106781*self._size),
2843 path._lineto(x, y+0.707106781*self._size))
2845 def square(self, x, y):
2846 return (path._moveto(x-0.5*self._size, y-0.5 * self._size),
2847 path._lineto(x+0.5*self._size, y-0.5 * self._size),
2848 path._lineto(x+0.5*self._size, y+0.5 * self._size),
2849 path._lineto(x-0.5*self._size, y+0.5 * self._size),
2850 path.closepath())
2852 def triangle(self, x, y):
2853 return (path._moveto(x-0.759835685*self._size, y-0.438691337*self._size),
2854 path._lineto(x+0.759835685*self._size, y-0.438691337*self._size),
2855 path._lineto(x, y+0.877382675*self._size),
2856 path.closepath())
2858 def circle(self, x, y):
2859 return (path._arc(x, y, 0.564189583*self._size, 0, 360),
2860 path.closepath())
2862 def diamond(self, x, y):
2863 return (path._moveto(x-0.537284965*self._size, y),
2864 path._lineto(x, y-0.930604859*self._size),
2865 path._lineto(x+0.537284965*self._size, y),
2866 path._lineto(x, y+0.930604859*self._size),
2867 path.closepath())
2869 def __init__(self, symbol=_nodefault,
2870 size="0.2 cm", symbolattrs=canvas.stroked(),
2871 errorscale=0.5, errorbarattrs=(),
2872 lineattrs=None):
2873 self.size_str = size
2874 if symbol is _nodefault:
2875 self._symbol = changesymbol.cross()
2876 else:
2877 self._symbol = symbol
2878 self._symbolattrs = symbolattrs
2879 self.errorscale = errorscale
2880 self._errorbarattrs = errorbarattrs
2881 self._lineattrs = lineattrs
2883 def iteratedict(self):
2884 result = {}
2885 result["symbol"] = _iterateattr(self._symbol)
2886 result["size"] = _iterateattr(self.size_str)
2887 result["symbolattrs"] = _iterateattrs(self._symbolattrs)
2888 result["errorscale"] = _iterateattr(self.errorscale)
2889 result["errorbarattrs"] = _iterateattrs(self._errorbarattrs)
2890 result["lineattrs"] = _iterateattrs(self._lineattrs)
2891 return result
2893 def iterate(self):
2894 return symbol(**self.iteratedict())
2896 def othercolumnkey(self, key, index):
2897 raise ValueError("unsuitable key '%s'" % key)
2899 def setcolumns(self, graph, columns):
2900 def checkpattern(key, index, pattern, iskey, isindex):
2901 if key is not None:
2902 match = pattern.match(key)
2903 if match:
2904 if isindex is not None: raise ValueError("multiple key specification")
2905 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
2906 key = None
2907 iskey = match.groups()[0]
2908 isindex = index
2909 return key, iskey, isindex
2911 self.xi = self.xmini = self.xmaxi = None
2912 self.dxi = self.dxmini = self.dxmaxi = None
2913 self.yi = self.ymini = self.ymaxi = None
2914 self.dyi = self.dymini = self.dymaxi = None
2915 self.xkey = self.ykey = None
2916 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
2917 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
2918 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
2919 XMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
2920 YMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
2921 XMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
2922 YMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
2923 DXPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
2924 DYPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
2925 DXMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
2926 DYMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
2927 DXMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
2928 DYMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
2929 for key, index in columns.items():
2930 key, self.xkey, self.xi = checkpattern(key, index, XPattern, self.xkey, self.xi)
2931 key, self.ykey, self.yi = checkpattern(key, index, YPattern, self.ykey, self.yi)
2932 key, self.xkey, self.xmini = checkpattern(key, index, XMinPattern, self.xkey, self.xmini)
2933 key, self.ykey, self.ymini = checkpattern(key, index, YMinPattern, self.ykey, self.ymini)
2934 key, self.xkey, self.xmaxi = checkpattern(key, index, XMaxPattern, self.xkey, self.xmaxi)
2935 key, self.ykey, self.ymaxi = checkpattern(key, index, YMaxPattern, self.ykey, self.ymaxi)
2936 key, self.xkey, self.dxi = checkpattern(key, index, DXPattern, self.xkey, self.dxi)
2937 key, self.ykey, self.dyi = checkpattern(key, index, DYPattern, self.ykey, self.dyi)
2938 key, self.xkey, self.dxmini = checkpattern(key, index, DXMinPattern, self.xkey, self.dxmini)
2939 key, self.ykey, self.dymini = checkpattern(key, index, DYMinPattern, self.ykey, self.dymini)
2940 key, self.xkey, self.dxmaxi = checkpattern(key, index, DXMaxPattern, self.xkey, self.dxmaxi)
2941 key, self.ykey, self.dymaxi = checkpattern(key, index, DYMaxPattern, self.ykey, self.dymaxi)
2942 if key is not None:
2943 self.othercolumnkey(key, index)
2944 if None in (self.xkey, self.ykey): raise ValueError("incomplete axis specification")
2945 if (len(filter(None, (self.xmini, self.dxmini, self.dxi))) > 1 or
2946 len(filter(None, (self.ymini, self.dymini, self.dyi))) > 1 or
2947 len(filter(None, (self.xmaxi, self.dxmaxi, self.dxi))) > 1 or
2948 len(filter(None, (self.ymaxi, self.dymaxi, self.dyi))) > 1):
2949 raise ValueError("multiple errorbar definition")
2950 if ((self.xi is None and self.dxi is not None) or
2951 (self.yi is None and self.dyi is not None) or
2952 (self.xi is None and self.dxmini is not None) or
2953 (self.yi is None and self.dymini is not None) or
2954 (self.xi is None and self.dxmaxi is not None) or
2955 (self.yi is None and self.dymaxi is not None)):
2956 raise ValueError("errorbar definition start value missing")
2957 self.xaxis = graph.axes[self.xkey]
2958 self.yaxis = graph.axes[self.ykey]
2960 def minmidmax(self, point, i, mini, maxi, di, dmini, dmaxi):
2961 min = max = mid = None
2962 try:
2963 mid = point[i] + 0.0
2964 except (TypeError, ValueError):
2965 pass
2966 try:
2967 if di is not None: min = point[i] - point[di]
2968 elif dmini is not None: min = point[i] - point[dmini]
2969 elif mini is not None: min = point[mini] + 0.0
2970 except (TypeError, ValueError):
2971 pass
2972 try:
2973 if di is not None: max = point[i] + point[di]
2974 elif dmaxi is not None: max = point[i] + point[dmaxi]
2975 elif maxi is not None: max = point[maxi] + 0.0
2976 except (TypeError, ValueError):
2977 pass
2978 if mid is not None:
2979 if min is not None and min > mid: raise ValueError("minimum error in errorbar")
2980 if max is not None and max < mid: raise ValueError("maximum error in errorbar")
2981 else:
2982 if min is not None and max is not None and min > max: raise ValueError("minimum/maximum error in errorbar")
2983 return min, mid, max
2985 def keyrange(self, points, i, mini, maxi, di, dmini, dmaxi):
2986 allmin = allmax = None
2987 if filter(None, (mini, maxi, di, dmini, dmaxi)) is not None:
2988 for point in points:
2989 min, mid, max = self.minmidmax(point, i, mini, maxi, di, dmini, dmaxi)
2990 if min is not None and (allmin is None or min < allmin): allmin = min
2991 if mid is not None and (allmin is None or mid < allmin): allmin = mid
2992 if mid is not None and (allmax is None or mid > allmax): allmax = mid
2993 if max is not None and (allmax is None or max > allmax): allmax = max
2994 else:
2995 for point in points:
2996 try:
2997 value = point[i] + 0.0
2998 if allmin is None or point[i] < allmin: allmin = point[i]
2999 if allmax is None or point[i] > allmax: allmax = point[i]
3000 except (TypeError, ValueError):
3001 pass
3002 return allmin, allmax
3004 def getranges(self, points):
3005 xmin, xmax = self.keyrange(points, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
3006 ymin, ymax = self.keyrange(points, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
3007 return {self.xkey: (xmin, xmax), self.ykey: (ymin, ymax)}
3009 def _drawerrorbar(self, graph, topleft, top, topright,
3010 left, center, right,
3011 bottomleft, bottom, bottomright, point=None):
3012 if left is not None:
3013 if right is not None:
3014 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3015 left2 = graph._addpos(*(left+(0, self._errorsize)))
3016 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3017 right2 = graph._addpos(*(right+(0, self._errorsize)))
3018 graph.stroke(path.path(path._moveto(*left1),
3019 graph._connect(*(left1+left2)),
3020 path._moveto(*left),
3021 graph._connect(*(left+right)),
3022 path._moveto(*right1),
3023 graph._connect(*(right1+right2))),
3024 *self.errorbarattrs)
3025 elif center is not None:
3026 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3027 left2 = graph._addpos(*(left+(0, self._errorsize)))
3028 graph.stroke(path.path(path._moveto(*left1),
3029 graph._connect(*(left1+left2)),
3030 path._moveto(*left),
3031 graph._connect(*(left+center))),
3032 *self.errorbarattrs)
3033 else:
3034 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3035 left2 = graph._addpos(*(left+(0, self._errorsize)))
3036 left3 = graph._addpos(*(left+(self._errorsize, 0)))
3037 graph.stroke(path.path(path._moveto(*left1),
3038 graph._connect(*(left1+left2)),
3039 path._moveto(*left),
3040 graph._connect(*(left+left3))),
3041 *self.errorbarattrs)
3042 if right is not None and left is None:
3043 if center is not None:
3044 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3045 right2 = graph._addpos(*(right+(0, self._errorsize)))
3046 graph.stroke(path.path(path._moveto(*right1),
3047 graph._connect(*(right1+right2)),
3048 path._moveto(*right),
3049 graph._connect(*(right+center))),
3050 *self.errorbarattrs)
3051 else:
3052 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3053 right2 = graph._addpos(*(right+(0, self._errorsize)))
3054 right3 = graph._addpos(*(right+(-self._errorsize, 0)))
3055 graph.stroke(path.path(path._moveto(*right1),
3056 graph._connect(*(right1+right2)),
3057 path._moveto(*right),
3058 graph._connect(*(right+right3))),
3059 *self.errorbarattrs)
3061 if bottom is not None:
3062 if top is not None:
3063 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3064 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3065 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3066 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3067 graph.stroke(path.path(path._moveto(*bottom1),
3068 graph._connect(*(bottom1+bottom2)),
3069 path._moveto(*bottom),
3070 graph._connect(*(bottom+top)),
3071 path._moveto(*top1),
3072 graph._connect(*(top1+top2))),
3073 *self.errorbarattrs)
3074 elif center is not None:
3075 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3076 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3077 graph.stroke(path.path(path._moveto(*bottom1),
3078 graph._connect(*(bottom1+bottom2)),
3079 path._moveto(*bottom),
3080 graph._connect(*(bottom+center))),
3081 *self.errorbarattrs)
3082 else:
3083 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3084 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3085 bottom3 = graph._addpos(*(bottom+(0, self._errorsize)))
3086 graph.stroke(path.path(path._moveto(*bottom1),
3087 graph._connect(*(bottom1+bottom2)),
3088 path._moveto(*bottom),
3089 graph._connect(*(bottom+bottom3))),
3090 *self.errorbarattrs)
3091 if top is not None and bottom is None:
3092 if center is not None:
3093 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3094 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3095 graph.stroke(path.path(path._moveto(*top1),
3096 graph._connect(*(top1+top2)),
3097 path._moveto(*top),
3098 graph._connect(*(top+center))),
3099 *self.errorbarattrs)
3100 else:
3101 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3102 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3103 top3 = graph._addpos(*(top+(0, -self._errorsize)))
3104 graph.stroke(path.path(path._moveto(*top1),
3105 graph._connect(*(top1+top2)),
3106 path._moveto(*top),
3107 graph._connect(*(top+top3))),
3108 *self.errorbarattrs)
3109 if bottomleft is not None:
3110 if topleft is not None and bottomright is None:
3111 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
3112 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
3113 graph.stroke(path.path(path._moveto(*bottomleft1),
3114 graph._connect(*(bottomleft1+bottomleft)),
3115 graph._connect(*(bottomleft+topleft)),
3116 graph._connect(*(topleft+topleft1))),
3117 *self.errorbarattrs)
3118 elif bottomright is not None and topleft is None:
3119 bottomleft1 = graph._addpos(*(bottomleft+(0, self._errorsize)))
3120 bottomright1 = graph._addpos(*(bottomright+(0, self._errorsize)))
3121 graph.stroke(path.path(path._moveto(*bottomleft1),
3122 graph._connect(*(bottomleft1+bottomleft)),
3123 graph._connect(*(bottomleft+bottomright)),
3124 graph._connect(*(bottomright+bottomright1))),
3125 *self.errorbarattrs)
3126 elif bottomright is None and topleft is None:
3127 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
3128 bottomleft2 = graph._addpos(*(bottomleft+(0, self._errorsize)))
3129 graph.stroke(path.path(path._moveto(*bottomleft1),
3130 graph._connect(*(bottomleft1+bottomleft)),
3131 graph._connect(*(bottomleft+bottomleft2))),
3132 *self.errorbarattrs)
3133 if topright is not None:
3134 if bottomright is not None and topleft is None:
3135 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
3136 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
3137 graph.stroke(path.path(path._moveto(*topright1),
3138 graph._connect(*(topright1+topright)),
3139 graph._connect(*(topright+bottomright)),
3140 graph._connect(*(bottomright+bottomright1))),
3141 *self.errorbarattrs)
3142 elif topleft is not None and bottomright is None:
3143 topright1 = graph._addpos(*(topright+(0, -self._errorsize)))
3144 topleft1 = graph._addpos(*(topleft+(0, -self._errorsize)))
3145 graph.stroke(path.path(path._moveto(*topright1),
3146 graph._connect(*(topright1+topright)),
3147 graph._connect(*(topright+topleft)),
3148 graph._connect(*(topleft+topleft1))),
3149 *self.errorbarattrs)
3150 elif topleft is None and bottomright is None:
3151 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
3152 topright2 = graph._addpos(*(topright+(0, -self._errorsize)))
3153 graph.stroke(path.path(path._moveto(*topright1),
3154 graph._connect(*(topright1+topright)),
3155 graph._connect(*(topright+topright2))),
3156 *self.errorbarattrs)
3157 if bottomright is not None and bottomleft is None and topright is None:
3158 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
3159 bottomright2 = graph._addpos(*(bottomright+(0, self._errorsize)))
3160 graph.stroke(path.path(path._moveto(*bottomright1),
3161 graph._connect(*(bottomright1+bottomright)),
3162 graph._connect(*(bottomright+bottomright2))),
3163 *self.errorbarattrs)
3164 if topleft is not None and bottomleft is None and topright is None:
3165 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
3166 topleft2 = graph._addpos(*(topleft+(0, -self._errorsize)))
3167 graph.stroke(path.path(path._moveto(*topleft1),
3168 graph._connect(*(topleft1+topleft)),
3169 graph._connect(*(topleft+topleft2))),
3170 *self.errorbarattrs)
3171 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
3172 graph.stroke(path.path(path._moveto(*bottomleft),
3173 graph._connect(*(bottomleft+bottomright)),
3174 graph._connect(*(bottomright+topright)),
3175 graph._connect(*(topright+topleft)),
3176 path.closepath()),
3177 *self.errorbarattrs)
3179 def _drawsymbol(self, graph, x, y, point=None):
3180 if x is not None and y is not None:
3181 graph.draw(path.path(*self.symbol(self, x, y)), *self.symbolattrs)
3183 def drawsymbol(self, graph, x, y, point=None):
3184 self._drawsymbol(graph, unit.topt(x), unit.topt(y), point)
3186 def drawpoints(self, graph, points):
3187 xaxismin, xaxismax = self.xaxis.getdatarange()
3188 yaxismin, yaxismax = self.yaxis.getdatarange()
3189 self.size = unit.length(_getattr(self.size_str), default_type="v")
3190 self._size = unit.topt(self.size)
3191 self.symbol = _getattr(self._symbol)
3192 self.symbolattrs = _getattrs(_ensuresequence(self._symbolattrs))
3193 self.errorbarattrs = _getattrs(_ensuresequence(self._errorbarattrs))
3194 self._errorsize = self.errorscale * self._size
3195 self.errorsize = self.errorscale * self.size
3196 self.lineattrs = _getattrs(_ensuresequence(self._lineattrs))
3197 if self._lineattrs is not None:
3198 clipcanvas = graph.clipcanvas()
3199 lineels = []
3200 haserror = filter(None, (self.xmini, self.ymini, self.xmaxi, self.ymaxi,
3201 self.dxi, self.dyi, self.dxmini, self.dymini, self.dxmaxi, self.dymaxi)) is not None
3202 moveto = 1
3203 for point in points:
3204 drawsymbol = 1
3205 xmin, x, xmax = self.minmidmax(point, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
3206 ymin, y, ymax = self.minmidmax(point, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
3207 if x is not None and x < xaxismin: drawsymbol = 0
3208 elif x is not None and x > xaxismax: drawsymbol = 0
3209 elif y is not None and y < yaxismin: drawsymbol = 0
3210 elif y is not None and y > yaxismax: drawsymbol = 0
3211 elif haserror:
3212 if xmin is not None and xmin < xaxismin: drawsymbol = 0
3213 elif xmax is not None and xmax < xaxismin: drawsymbol = 0
3214 elif xmax is not None and xmax > xaxismax: drawsymbol = 0
3215 elif xmin is not None and xmin > xaxismax: drawsymbol = 0
3216 elif ymin is not None and ymin < yaxismin: drawsymbol = 0
3217 elif ymax is not None and ymax < yaxismin: drawsymbol = 0
3218 elif ymax is not None and ymax > yaxismax: drawsymbol = 0
3219 elif ymin is not None and ymin > yaxismax: drawsymbol = 0
3220 xpos=ypos=topleft=top=topright=left=center=right=bottomleft=bottom=bottomright=None
3221 if x is not None and y is not None:
3222 try:
3223 center = xpos, ypos = graph._pos(x, y, xaxis=self.xaxis, yaxis=self.yaxis)
3224 except ValueError:
3225 pass
3226 if haserror:
3227 if y is not None:
3228 if xmin is not None: left = graph._pos(xmin, y, xaxis=self.xaxis, yaxis=self.yaxis)
3229 if xmax is not None: right = graph._pos(xmax, y, xaxis=self.xaxis, yaxis=self.yaxis)
3230 if x is not None:
3231 if ymax is not None: top = graph._pos(x, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3232 if ymin is not None: bottom = graph._pos(x, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3233 if x is None or y is None:
3234 if ymax is not None:
3235 if xmin is not None: topleft = graph._pos(xmin, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3236 if xmax is not None: topright = graph._pos(xmax, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3237 if ymin is not None:
3238 if xmin is not None: bottomleft = graph._pos(xmin, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3239 if xmax is not None: bottomright = graph._pos(xmax, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3240 if drawsymbol:
3241 if self._errorbarattrs is not None and haserror:
3242 self._drawerrorbar(graph, topleft, top, topright,
3243 left, center, right,
3244 bottomleft, bottom, bottomright, point)
3245 if self._symbolattrs is not None:
3246 self._drawsymbol(graph, xpos, ypos, point)
3247 if xpos is not None and ypos is not None:
3248 if moveto:
3249 lineels.append(path._moveto(xpos, ypos))
3250 moveto = 0
3251 else:
3252 lineels.append(path._lineto(xpos, ypos))
3253 else:
3254 moveto = 1
3255 self.path = path.path(*lineels)
3256 if self._lineattrs is not None:
3257 clipcanvas.stroke(self.path, *self.lineattrs)
3260 class changesymbol(changesequence): pass
3263 class _changesymbolcross(changesymbol):
3264 defaultsequence = (symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond)
3267 class _changesymbolplus(changesymbol):
3268 defaultsequence = (symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross)
3271 class _changesymbolsquare(changesymbol):
3272 defaultsequence = (symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus)
3275 class _changesymboltriangle(changesymbol):
3276 defaultsequence = (symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square)
3279 class _changesymbolcircle(changesymbol):
3280 defaultsequence = (symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle)
3283 class _changesymboldiamond(changesymbol):
3284 defaultsequence = (symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle)
3287 class _changesymbolsquaretwice(changesymbol):
3288 defaultsequence = (symbol.square, symbol.square, symbol.triangle, symbol.triangle,
3289 symbol.circle, symbol.circle, symbol.diamond, symbol.diamond)
3292 class _changesymboltriangletwice(changesymbol):
3293 defaultsequence = (symbol.triangle, symbol.triangle, symbol.circle, symbol.circle,
3294 symbol.diamond, symbol.diamond, symbol.square, symbol.square)
3297 class _changesymbolcircletwice(changesymbol):
3298 defaultsequence = (symbol.circle, symbol.circle, symbol.diamond, symbol.diamond,
3299 symbol.square, symbol.square, symbol.triangle, symbol.triangle)
3302 class _changesymboldiamondtwice(changesymbol):
3303 defaultsequence = (symbol.diamond, symbol.diamond, symbol.square, symbol.square,
3304 symbol.triangle, symbol.triangle, symbol.circle, symbol.circle)
3307 changesymbol.cross = _changesymbolcross
3308 changesymbol.plus = _changesymbolplus
3309 changesymbol.square = _changesymbolsquare
3310 changesymbol.triangle = _changesymboltriangle
3311 changesymbol.circle = _changesymbolcircle
3312 changesymbol.diamond = _changesymboldiamond
3313 changesymbol.squaretwice = _changesymbolsquaretwice
3314 changesymbol.triangletwice = _changesymboltriangletwice
3315 changesymbol.circletwice = _changesymbolcircletwice
3316 changesymbol.diamondtwice = _changesymboldiamondtwice
3319 class line(symbol):
3321 def __init__(self, lineattrs=_nodefault):
3322 if lineattrs is _nodefault:
3323 lineattrs = changelinestyle()
3324 symbol.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
3327 class rect(symbol):
3329 def __init__(self, gradient=color.gradient.Gray):
3330 self.gradient = gradient
3331 self.colorindex = None
3332 symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
3334 def iterate(self):
3335 raise RuntimeError("style is not iterateable")
3337 def othercolumnkey(self, key, index):
3338 if key == "color":
3339 self.colorindex = index
3340 else:
3341 symbol.othercolumnkey(self, key, index)
3343 def _drawerrorbar(self, graph, topleft, top, topright,
3344 left, center, right,
3345 bottomleft, bottom, bottomright, point=None):
3346 color = point[self.colorindex]
3347 if color is not None:
3348 if color != self.lastcolor:
3349 self.rectclipcanvas.set(self.gradient.getcolor(color))
3350 if bottom is not None and left is not None:
3351 bottomleft = left[0], bottom[1]
3352 if bottom is not None and right is not None:
3353 bottomright = right[0], bottom[1]
3354 if top is not None and right is not None:
3355 topright = right[0], top[1]
3356 if top is not None and left is not None:
3357 topleft = left[0], top[1]
3358 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
3359 self.rectclipcanvas.fill(path.path(path._moveto(*bottomleft),
3360 graph._connect(*(bottomleft+bottomright)),
3361 graph._connect(*(bottomright+topright)),
3362 graph._connect(*(topright+topleft)),
3363 path.closepath()))
3365 def drawpoints(self, graph, points):
3366 if self.colorindex is None:
3367 raise RuntimeError("column 'color' not set")
3368 self.lastcolor = None
3369 self.rectclipcanvas = graph.clipcanvas()
3370 symbol.drawpoints(self, graph, points)
3374 class text(symbol):
3376 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=tex.halign.center, **args):
3377 self.textindex = None
3378 self.textdx_str = textdx
3379 self.textdy_str = textdy
3380 self._textattrs = textattrs
3381 symbol.__init__(self, **args)
3383 def iteratedict(self):
3384 result = symbol.iteratedict()
3385 result["textattrs"] = _iterateattr(self._textattrs)
3386 return result
3388 def iterate(self):
3389 return textsymbol(**self.iteratedict())
3391 def othercolumnkey(self, key, index):
3392 if key == "text":
3393 self.textindex = index
3394 else:
3395 symbol.othercolumnkey(self, key, index)
3397 def _drawsymbol(self, graph, x, y, point=None):
3398 symbol._drawsymbol(self, graph, x, y, point)
3399 if None not in (x, y, point[self.textindex], self._textattrs):
3400 graph.tex._text(x + self._textdx, y + self._textdy, str(point[self.textindex]), *_ensuresequence(self.textattrs))
3402 def drawpoints(self, graph, points):
3403 self.textdx = unit.length(_getattr(self.textdx_str), default_type="v")
3404 self.textdy = unit.length(_getattr(self.textdy_str), default_type="v")
3405 self._textdx = unit.topt(self.textdx)
3406 self._textdy = unit.topt(self.textdy)
3407 if self._textattrs is not None:
3408 self.textattrs = _getattr(self._textattrs)
3409 if self.textindex is None:
3410 raise RuntimeError("column 'text' not set")
3411 symbol.drawpoints(self, graph, points)
3414 class arrow(symbol):
3416 def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
3417 self.linelength_str = linelength
3418 self.arrowsize_str = arrowsize
3419 self.arrowattrs = arrowattrs
3420 self.arrowdict = arrowdict
3421 self.epsilon = epsilon
3422 self.sizeindex = self.angleindex = None
3423 symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
3425 def iterate(self):
3426 raise RuntimeError("style is not iterateable")
3428 def othercolumnkey(self, key, index):
3429 if key == "size":
3430 self.sizeindex = index
3431 elif key == "angle":
3432 self.angleindex = index
3433 else:
3434 symbol.othercolumnkey(self, key, index)
3436 def _drawsymbol(self, graph, x, y, point=None):
3437 if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
3438 if point[self.sizeindex] > self.epsilon:
3439 dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
3440 x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
3441 y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
3442 x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
3443 y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
3444 graph.stroke(path.line(x1, y1, x2, y2),
3445 canvas.earrow(self.arrowsize*point[self.sizeindex],
3446 **self.arrowdict),
3447 *_ensuresequence(self.arrowattrs))
3449 def drawpoints(self, graph, points):
3450 self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
3451 self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
3452 self._arrowsize = unit.topt(self.arrowsize)
3453 self._linelength = unit.topt(self.linelength)
3454 if self.sizeindex is None:
3455 raise RuntimeError("column 'size' not set")
3456 if self.angleindex is None:
3457 raise RuntimeError("column 'angle' not set")
3458 symbol.drawpoints(self, graph, points)
3461 class _bariterator(changeattr):
3463 def attr(self, index):
3464 return index, self.counter
3467 class bar:
3469 def __init__(self, fromzero=1, stacked=0, xbar=0,
3470 barattrs=(canvas.stroked(color.gray.black), changecolor.Rainbow()),
3471 _bariterator=_bariterator(), _previousbar=None):
3472 self.fromzero = fromzero
3473 self.stacked = stacked
3474 self.xbar = xbar
3475 self._barattrs = barattrs
3476 self.bariterator = _bariterator
3477 self.previousbar = _previousbar
3479 def iteratedict(self):
3480 result = {}
3481 result["barattrs"] = _iterateattrs(self._barattrs)
3482 return result
3484 def iterate(self):
3485 return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
3486 _bariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
3488 def setcolumns(self, graph, columns):
3489 def checkpattern(key, index, pattern, iskey, isindex):
3490 if key is not None:
3491 match = pattern.match(key)
3492 if match:
3493 if isindex is not None: raise ValueError("multiple key specification")
3494 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
3495 key = None
3496 iskey = match.groups()[0]
3497 isindex = index
3498 return key, iskey, isindex
3500 xkey = ykey = None
3501 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
3502 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3503 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3504 xi = yi = None
3505 for key, index in columns.items():
3506 key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
3507 key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
3508 if key is not None:
3509 self.othercolumnkey(key, index)
3510 if None in (xkey, ykey): raise ValueError("incomplete axis specification")
3511 if self.xbar:
3512 self.nkey, self.ni = ykey, yi
3513 self.vkey, self.vi = xkey, xi
3514 else:
3515 self.nkey, self.ni = xkey, xi
3516 self.vkey, self.vi = ykey, yi
3517 self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
3519 def getranges(self, points):
3520 index, count = _getattr(self.bariterator)
3521 if count != 1 and self.stacked != 1:
3522 if self.stacked > 1:
3523 index = divmod(index, self.stacked)[0] # TODO: use this
3525 vmin = vmax = None
3526 for point in points:
3527 try:
3528 v = point[self.vi] + 0.0
3529 if vmin is None or v < vmin: vmin = v
3530 if vmax is None or v > vmax: vmax = v
3531 except (TypeError, ValueError):
3532 pass
3533 else:
3534 if count == 1:
3535 self.naxis.setname(point[self.ni])
3536 else:
3537 self.naxis.setname(point[self.ni], index)
3538 if self.fromzero:
3539 if vmin > 0: vmin = 0
3540 if vmax < 0: vmax = 0
3541 return {self.vkey: (vmin, vmax)}
3543 def drawpoints(self, graph, points):
3544 index, count = _getattr(self.bariterator)
3545 dostacked = (self.stacked != 0 and
3546 (self.stacked == 1 or divmod(index, self.stacked)[1]) and
3547 (self.stacked != 1 or index))
3548 if self.stacked > 1:
3549 index = divmod(index, self.stacked)[0]
3550 vmin, vmax = self.vaxis.getdatarange()
3551 self.barattrs = _getattrs(_ensuresequence(self._barattrs))
3552 if self.stacked:
3553 self.stackedvalue = {}
3554 for point in points:
3555 try:
3556 n = point[self.ni]
3557 v = point[self.vi]
3558 if self.stacked:
3559 self.stackedvalue[n] = v
3560 if count != 1 and self.stacked != 1:
3561 minid = (n, index, 0)
3562 maxid = (n, index, 1)
3563 else:
3564 minid = (n, 0)
3565 maxid = (n, 1)
3566 if self.xbar:
3567 x1pos, y1pos = graph._pos(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
3568 x2pos, y2pos = graph._pos(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
3569 else:
3570 x1pos, y1pos = graph._pos(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
3571 x2pos, y2pos = graph._pos(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
3572 if dostacked:
3573 if self.xbar:
3574 x3pos, y3pos = graph._pos(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
3575 x4pos, y4pos = graph._pos(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
3576 else:
3577 x3pos, y3pos = graph._pos(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
3578 x4pos, y4pos = graph._pos(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
3579 else:
3580 if self.fromzero:
3581 if self.xbar:
3582 x3pos, y3pos = graph._pos(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
3583 x4pos, y4pos = graph._pos(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
3584 else:
3585 x3pos, y3pos = graph._pos(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
3586 x4pos, y4pos = graph._pos(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
3587 else:
3588 x3pos, y3pos = self.naxis._vtickpoint(self.naxis, self.naxis.convert(maxid))
3589 x4pos, y4pos = self.naxis._vtickpoint(self.naxis, self.naxis.convert(minid))
3590 graph.fill(path.path(path._moveto(x1pos, y1pos),
3591 graph._connect(x1pos, y1pos, x2pos, y2pos),
3592 graph._connect(x2pos, y2pos, x3pos, y3pos),
3593 graph._connect(x3pos, y3pos, x4pos, y4pos),
3594 graph._connect(x4pos, y4pos, x1pos, y1pos), # no closepath (might not be straight)
3595 path.closepath()), *self.barattrs)
3596 except (TypeError, ValueError): pass
3599 #class surface:
3601 # def setcolumns(self, graph, columns):
3602 # self.columns = columns
3604 # def getranges(self, points):
3605 # return {"x": (0, 10), "y": (0, 10), "z": (0, 1)}
3607 # def drawpoints(self, graph, points):
3608 # pass
3612 ################################################################################
3613 # data
3614 ################################################################################
3617 import data as datamodule # well, ugly ???
3619 class data:
3621 defaultstyle = symbol
3623 def __init__(self, file, **columns):
3624 if _isstring(file):
3625 self.data = datamodule.datafile(file)
3626 else:
3627 self.data = file
3628 self.columns = {}
3629 usedkeys = []
3630 for key, column in columns.items():
3631 try:
3632 self.columns[key] = self.data.getcolumnno(column)
3633 except datamodule.ColumnError:
3634 self.columns[key] = len(self.data.titles)
3635 usedkeys.extend(self.data._addcolumn(column, **columns))
3636 for usedkey in usedkeys:
3637 if usedkey in self.columns.keys():
3638 del self.columns[usedkey]
3640 def setstyle(self, graph, style):
3641 self.style = style
3642 self.style.setcolumns(graph, self.columns)
3644 def getranges(self):
3645 return self.style.getranges(self.data.data)
3647 def setranges(self, ranges):
3648 pass
3650 def draw(self, graph):
3651 self.style.drawpoints(graph, self.data.data)
3654 class function:
3656 defaultstyle = line
3658 def __init__(self, expression, min=None, max=None, points=100, parser=mathtree.parser(), extern=None):
3659 self.min = min
3660 self.max = max
3661 self.points = points
3662 self.extern = extern
3663 self.result, expression = expression.split("=")
3664 self.mathtree = parser.parse(expression, extern=self.extern)
3665 if extern is None:
3666 self.variable, = self.mathtree.VarList()
3667 else:
3668 self.variable = None
3669 for variable in self.mathtree.VarList():
3670 if variable not in self.extern.keys():
3671 if self.variable is None:
3672 self.variable = variable
3673 else:
3674 raise ValueError("multiple variables found (identifiers might be externally defined)")
3675 if self.variable is None:
3676 raise ValueError("no variable found (identifiers are all defined externally)")
3677 self.evalranges = 0
3679 def setstyle(self, graph, style):
3680 self.xaxis = graph.axes[self.variable]
3681 self.style = style
3682 self.style.setcolumns(graph, {self.variable: 0, self.result: 1})
3684 def getranges(self):
3685 if self.evalranges:
3686 return self.style.getranges(self.data)
3687 if None not in (self.min, self.max):
3688 return {self.variable: (self.min, self.max)}
3690 def setranges(self, ranges):
3691 if ranges.has_key(self.variable):
3692 min, max = ranges[self.variable]
3693 if self.min is not None: min = self.min
3694 if self.max is not None: max = self.max
3695 vmin = self.xaxis.convert(min)
3696 vmax = self.xaxis.convert(max)
3697 self.data = []
3698 for i in range(self.points):
3699 x = self.xaxis.invert(vmin + (vmax-vmin)*i / (self.points-1.0))
3700 try:
3701 y = self.mathtree.Calc({self.variable: x}, self.extern)
3702 except (ArithmeticError, ValueError):
3703 y = None
3704 self.data.append((x, y))
3705 self.evalranges = 1
3707 def draw(self, graph):
3708 self.style.drawpoints(graph, self.data)
3711 class paramfunction:
3713 defaultstyle = line
3715 def __init__(self, varname, min, max, expression, points=100, parser=mathtree.parser(), extern=None):
3716 self.varname = varname
3717 self.min = min
3718 self.max = max
3719 self.points = points
3720 self.expression = {}
3721 self.mathtrees = {}
3722 varlist, expressionlist = expression.split("=")
3723 parsestr = mathtree.ParseStr(expressionlist)
3724 for key in varlist.split(","):
3725 key = key.strip()
3726 if self.mathtrees.has_key(key):
3727 raise ValueError("multiple assignment in tuple")
3728 try:
3729 self.mathtrees[key] = parser.ParseMathTree(parsestr, extern)
3730 break
3731 except mathtree.CommaFoundMathTreeParseError, e:
3732 self.mathtrees[key] = e.MathTree
3733 else:
3734 raise ValueError("unpack tuple of wrong size")
3735 if len(varlist.split(",")) != len(self.mathtrees.keys()):
3736 raise ValueError("unpack tuple of wrong size")
3737 self.data = []
3738 for i in range(self.points):
3739 value = self.min + (self.max-self.min)*i / (self.points-1.0)
3740 line = []
3741 for key, tree in self.mathtrees.items():
3742 line.append(tree.Calc({self.varname: value}, extern))
3743 self.data.append(line)
3745 def setstyle(self, graph, style):
3746 self.style = style
3747 columns = {}
3748 for key, index in zip(self.mathtrees.keys(), xrange(sys.maxint)):
3749 columns[key] = index
3750 self.style.setcolumns(graph, columns)
3752 def getranges(self):
3753 return self.style.getranges(self.data)
3755 def setranges(self, ranges):
3756 pass
3758 def draw(self, graph):
3759 self.style.drawpoints(graph, self.data)