GPL -> Latex license
[PyX/mjg.git] / pyx / graph.py
blobf2323d18bbff96b8ad570a6920c43b1d23f0095b
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, box, canvas, path, unit, mathtree, color, helper
26 import text as textmodule
27 import data as datamodule
28 import trafo as trafomodule
31 goldenmean = 0.5 * (math.sqrt(5) + 1)
34 ################################################################################
35 # maps
36 ################################################################################
38 class _Imap:
39 """interface definition of a map
40 maps convert a value into another value by bijective transformation f"""
42 def convert(self, x):
43 "returns f(x)"
45 def invert(self, y):
46 "returns f^-1(y) where f^-1 is the inverse transformation (x=f^-1(f(x)) for all x)"
48 def setbasepoint(self, basepoints):
49 """set basepoints for the convertions
50 basepoints are tuples (x, y) with y == f(x) and x == f^-1(y)
51 the number of basepoints needed might depend on the transformation
52 usually two pairs are needed like for linear maps, logarithmic maps, etc."""
55 class _linmap:
56 "linear mapping"
57 __implements__ = _Imap
59 def setbasepoints(self, basepoints):
60 "method is part of the implementation of _Imap"
61 self.dydx = (basepoints[1][1] - basepoints[0][1]) / float(basepoints[1][0] - basepoints[0][0])
62 self.dxdy = (basepoints[1][0] - basepoints[0][0]) / float(basepoints[1][1] - basepoints[0][1])
63 self.x1 = basepoints[0][0]
64 self.y1 = basepoints[0][1]
65 return self
67 def convert(self, value):
68 "method is part of the implementation of _Imap"
69 return self.y1 + self.dydx * (value - self.x1)
71 def invert(self, value):
72 "method is part of the implementation of _Imap"
73 return self.x1 + self.dxdy * (value - self.y1)
76 class _logmap:
77 "logarithmic mapping"
78 __implements__ = _Imap
80 def setbasepoints(self, basepoints):
81 self.dydx = ((basepoints[1][1] - basepoints[0][1]) /
82 float(math.log(basepoints[1][0]) - math.log(basepoints[0][0])))
83 self.dxdy = ((math.log(basepoints[1][0]) - math.log(basepoints[0][0])) /
84 float(basepoints[1][1] - basepoints[0][1]))
85 self.x1 = math.log(basepoints[0][0])
86 self.y1 = basepoints[0][1]
87 return self
89 def convert(self, value):
90 return self.y1 + self.dydx * (math.log(value) - self.x1)
92 def invert(self, value):
93 return math.exp(self.x1 + self.dxdy * (value - self.y1))
97 ################################################################################
98 # partition schemes
99 # please note the nomenclature:
100 # - a partition is a ordered sequence of tick instances
101 # - a partition scheme is a class creating a single or several partitions
102 ################################################################################
105 class frac:
106 """fraction class for rational arithmetics
107 the axis partitioning uses rational arithmetics (with infinite accuracy)
108 basically it contains self.enum and self.denom"""
110 def __init__(self, enum, denom, power=None):
111 "for power!=None: frac=(enum/denom)**power"
112 if not helper.isinteger(enum) or not helper.isinteger(denom): raise TypeError("integer type expected")
113 if not denom: raise ZeroDivisionError("zero denominator")
114 if power != None:
115 if not helper.isinteger(power): raise TypeError("integer type expected")
116 if power >= 0:
117 self.enum = long(enum) ** power
118 self.denom = long(denom) ** power
119 else:
120 self.enum = long(denom) ** (-power)
121 self.denom = long(enum) ** (-power)
122 else:
123 self.enum = enum
124 self.denom = denom
126 def __cmp__(self, other):
127 if other is None:
128 return 1
129 return cmp(self.enum * other.denom, other.enum * self.denom)
131 def __abs__(self):
132 return frac(abs(self.enum), abs(self.denom))
134 def __mul__(self, other):
135 return frac(self.enum * other.enum, self.denom * other.denom)
137 def __div__(self, other):
138 return frac(self.enum * other.denom, self.denom * other.enum)
140 def __float__(self):
141 "caution: avoid final precision of floats"
142 return float(self.enum) / self.denom
144 def __str__(self):
145 return "%i/%i" % (self.enum, self.denom)
148 def _ensurefrac(arg):
149 """helper function to convert arg into a frac
150 - strings like 0.123, 1/-2, 1.23/34.21 are converted to a frac"""
151 # TODO: exponentials are not yet supported, e.g. 1e-10, etc.
152 # XXX: we don't need to force long on newer python versions
154 def createfrac(str):
155 "converts a string 0.123 into a frac"
156 commaparts = str.split(".")
157 for part in commaparts:
158 if not part.isdigit(): raise ValueError("non-digits found in '%s'" % part)
159 if len(commaparts) == 1:
160 return frac(long(commaparts[0]), 1)
161 elif len(commaparts) == 2:
162 result = frac(1, 10l, power=len(commaparts[1]))
163 result.enum = long(commaparts[0])*result.denom + long(commaparts[1])
164 return result
165 else: raise ValueError("multiple '.' found in '%s'" % str)
167 if helper.isstring(arg):
168 fraction = arg.split("/")
169 if len(fraction) > 2: raise ValueError("multiple '/' found in '%s'" % arg)
170 value = createfrac(fraction[0])
171 if len(fraction) == 2:
172 value2 = createfrac(fraction[1])
173 value = frac(value.enum * value2.denom, value.denom * value2.enum)
174 return value
175 if not isinstance(arg, frac): raise ValueError("can't convert argument to frac")
176 return arg
179 class tick(frac):
180 """tick class
181 a tick is a frac enhanced by
182 - self.ticklevel (0 = tick, 1 = subtick, etc.)
183 - self.labellevel (0 = label, 1 = sublabel, etc.)
184 - self.label (a string) and self.labelattrs (a list, defaults to [])
185 When ticklevel or labellevel is None, no tick or label is present at that value.
186 When label is None, it should be automatically created (and stored), once the
187 an axis painter needs it. Classes, which implement _Itexter do precisely that."""
189 def __init__(self, enum, denom, ticklevel=None, labellevel=None, label=None, labelattrs=[]):
190 frac.__init__(self, enum, denom)
191 self.ticklevel = ticklevel
192 self.labellevel = labellevel
193 self.label = label
194 self.labelattrs = helper.ensurelist(labelattrs)[:]
196 def merge(self, other):
197 """merges two ticks together:
198 - the lower ticklevel/labellevel wins
199 - when present, self.label is taken over; otherwise the others label is taken
200 (when self.label and other.label is present, the first wins)
201 - the ticks should be at the same position (otherwise it doesn't make sense)
202 -> this is NOT checked"""
203 # TODO: shouldn't we change the merging of label (always take the first one) ... if it's None, leave it that way
204 if self.ticklevel is None or (other.ticklevel is not None and other.ticklevel < self.ticklevel):
205 self.ticklevel = other.ticklevel
206 if self.labellevel is None or (other.labellevel is not None and other.labellevel < self.labellevel):
207 self.labellevel = other.labellevel
208 if self.label is None:
209 self.label = other.label
211 def __repr__(self):
212 return "tick(%r, %r, %s, %s, %s)" % (self.enum, self.denom, self.ticklevel, self.labellevel, self.label)
215 def _mergeticklists(list1, list2):
216 """helper function to merge tick lists
217 - return a merged list of ticks out of list1 and list2
218 - CAUTION: original lists have to be ordered
219 (the returned list is also ordered)
220 - CAUTION: original lists are modified and they share references to
221 the result list!"""
222 # TODO: improve this using bisect?!
223 i = 0
224 j = 0
225 try:
226 while 1: # we keep on going until we reach an index error
227 while list2[j] < list1[i]: # insert tick
228 list1.insert(i, list2[j])
229 i += 1
230 j += 1
231 if list2[j] == list1[i]: # merge tick
232 list1[i].merge(list2[j])
233 j += 1
234 i += 1
235 except IndexError:
236 if j < len(list2):
237 list1 += list2[j:]
238 return list1
241 def _mergelabels(ticks, labels):
242 """helper function to merge labels into ticks
243 - when labels is not None, the label of all ticks with
244 labellevel different from None are set
245 - labels need to be a sequence of sequences of strings,
246 where the first sequence contain the strings to be
247 used as labels for the ticks with labellevel 0,
248 the second sequence for labellevel 1, etc.
249 - when the maximum labellevel is 0, just a sequence of
250 strings might be provided as the labels argument
251 - IndexError is raised, when a sequence length doesn't match"""
252 if helper.issequenceofsequences(labels):
253 for label, level in zip(labels, xrange(sys.maxint)):
254 usetext = helper.ensuresequence(label)
255 i = 0
256 for tick in ticks:
257 if tick.labellevel == level:
258 tick.label = usetext[i]
259 i += 1
260 if i != len(usetext):
261 raise IndexError("wrong sequence length of labels at level %i" % level)
262 elif labels is not None:
263 usetext = helper.ensuresequence(labels)
264 i = 0
265 for tick in ticks:
266 if tick.labellevel == 0:
267 tick.label = usetext[i]
268 i += 1
269 if i != len(usetext):
270 raise IndexError("wrong sequence length of labels")
273 class _Ipart:
274 """interface definition of a partition scheme
275 partition schemes are used to create a list of ticks"""
277 def defaultpart(self, min, max, extendmin, extendmax):
278 """create a partition
279 - returns an ordered list of ticks for the interval min to max
280 - the interval is given in float numbers, thus an appropriate
281 conversion to rational numbers has to be performed
282 - extendmin and extendmax are booleans (integers)
283 - when extendmin or extendmax is set, the ticks might
284 extend the min-max range towards lower and higher
285 ranges, respectively"""
287 def lesspart(self):
288 """create another partition which contains less ticks
289 - this method is called several times after a call of defaultpart
290 - returns an ordered list of ticks with less ticks compared to
291 the partition returned by defaultpart and by previous calls
292 of lesspart
293 - the creation of a partition with strictly *less* ticks
294 is not to be taken serious
295 - the method might return None, when no other appropriate
296 partition can be created"""
299 def morepart(self):
300 """create another partition which contains more ticks
301 see lesspart, but increase the number of ticks"""
304 class manualpart:
305 """manual partition scheme
306 ticks and labels at positions explicitly provided to the constructor"""
308 __implements__ = _Ipart
310 def __init__(self, tickpos=None, labelpos=None, labels=None, mix=()):
311 """configuration of the partition scheme
312 - tickpos and labelpos should be a sequence of sequences, where
313 the first sequence contains the values to be used for
314 ticks with ticklevel/labellevel 0, the second sequence for
315 ticklevel/labellevel 1, etc.
316 - tickpos and labelpos values must be frac instances or
317 something convertable to fracs by the _ensurefrac function
318 - when the maximum ticklevel/labellevel is 0, just a sequence
319 might be provided in tickpos and labelpos
320 - when labelpos is None and tickpos is not None, the tick entries
321 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
322 - labels are applied to the resulting partition via the
323 mergelabels function (additional information available there)
324 - mix specifies another partition to be merged into the
325 created partition"""
326 if tickpos is None and labelpos is not None:
327 self.tickpos = helper.ensuresequence(helper.getsequenceno(labelpos, 0))
328 else:
329 self.tickpos = tickpos
330 if labelpos is None and tickpos is not None:
331 self.labelpos = helper.ensuresequence(helper.getsequenceno(tickpos, 0))
332 else:
333 self.labelpos = labelpos
334 self.labels = labels
335 self.mix = mix
337 def checkfraclist(self, *fracs):
338 "orders a list of fracs, equal entries are not allowed"
339 if not len(fracs): return ()
340 sorted = list(fracs)
341 sorted.sort()
342 last = sorted[0]
343 for item in sorted[1:]:
344 if last == item:
345 raise ValueError("duplicate entry found")
346 last = item
347 return sorted
349 def part(self):
350 "create the partition as described in the constructor"
351 ticks = list(self.mix)
352 if helper.issequenceofsequences(self.tickpos):
353 for fracs, level in zip(self.tickpos, xrange(sys.maxint)):
354 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, ticklevel=level)
355 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(fracs)))])
356 else:
357 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, ticklevel=0)
358 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(self.tickpos)))])
360 if helper.issequenceofsequences(self.labelpos):
361 for fracs, level in zip(self.labelpos, xrange(sys.maxint)):
362 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, labellevel = level)
363 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(fracs)))])
364 else:
365 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, labellevel = 0)
366 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(self.labelpos)))])
368 _mergelabels(ticks, self.labels)
370 return ticks
372 def defaultpart(self, min, max, extendmin, extendmax):
373 """method is part of the implementation of _Ipart
374 XXX: we do not take care of the parameters -> correct?"""
375 return self.part()
377 def lesspart(self):
378 "method is part of the implementation of _Ipart"
379 return None
381 def morepart(self):
382 "method is part of the implementation of _Ipart"
383 return None
386 class linpart:
387 """linear partition scheme
388 ticks and label distances are explicitly provided to the constructor"""
390 __implements__ = _Ipart
392 def __init__(self, tickdist=None, labeldist=None, labels=None, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
393 """configuration of the partition scheme
394 - tickdist and labeldist should be a sequence, where the first value
395 is the distance between ticks with ticklevel/labellevel 0,
396 the second sequence for ticklevel/labellevel 1, etc.;
397 a single entry is allowed without being a sequence
398 - tickdist and labeldist values must be frac instances or
399 something convertable to fracs by the _ensurefrac function
400 - when labeldist is None and tickdist is not None, the tick entries
401 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
402 - labels are applied to the resulting partition via the
403 mergelabels function (additional information available there)
404 - extendtick allows for the extension of the range given to the
405 defaultpart method to include the next tick with the specified
406 level (None turns off this feature); note, that this feature is
407 also disabled, when an axis prohibits its range extension by
408 the extendmin/extendmax variables given to the defaultpart method
409 - extendlabel is analogous to extendtick, but for labels
410 - epsilon allows for exceeding the axis range by this relative
411 value (relative to the axis range given to the defaultpart method)
412 without creating another tick specified by extendtick/extendlabel
413 - mix specifies another partition to be merged into the
414 created partition"""
415 if tickdist is None and labeldist is not None:
416 self.ticklist = (_ensurefrac(helper.ensuresequence(labeldist)[0]),)
417 else:
418 self.ticklist = map(_ensurefrac, helper.ensuresequence(tickdist))
419 if labeldist is None and tickdist is not None:
420 self.labellist = (_ensurefrac(helper.ensuresequence(tickdist)[0]),)
421 else:
422 self.labellist = map(_ensurefrac, helper.ensuresequence(labeldist))
423 self.labels = labels
424 self.extendtick = extendtick
425 self.extendlabel = extendlabel
426 self.epsilon = epsilon
427 self.mix = mix
429 def extendminmax(self, min, max, frac, extendmin, extendmax):
430 """return new min, max tuple extending the range min, max
431 - frac is the tick distance to be used
432 - extendmin and extendmax are booleans to allow for the extension"""
433 if extendmin:
434 min = float(frac) * math.floor(min / float(frac) + self.epsilon)
435 if extendmax:
436 max = float(frac) * math.ceil(max / float(frac) - self.epsilon)
437 return min, max
439 def getticks(self, min, max, frac, ticklevel=None, labellevel=None):
440 """return a list of equal spaced ticks
441 - the tick distance is frac, the ticklevel is set to ticklevel and
442 the labellevel is set to labellevel
443 - min, max is the range where ticks should be placed"""
444 imin = int(math.ceil(min / float(frac) - 0.5 * self.epsilon))
445 imax = int(math.floor(max / float(frac) + 0.5 * self.epsilon))
446 ticks = []
447 for i in range(imin, imax + 1):
448 ticks.append(tick(long(i) * frac.enum, frac.denom, ticklevel=ticklevel, labellevel=labellevel))
449 return ticks
451 def defaultpart(self, min, max, extendmin, extendmax):
452 "method is part of the implementation of _Ipart"
453 if self.extendtick is not None and len(self.ticklist) > self.extendtick:
454 min, max = self.extendminmax(min, max, self.ticklist[self.extendtick], extendmin, extendmax)
455 if self.extendlabel is not None and len(self.labellist) > self.extendlabel:
456 min, max = self.extendminmax(min, max, self.labellist[self.extendlabel], extendmin, extendmax)
458 ticks = list(self.mix)
459 for i in range(len(self.ticklist)):
460 ticks = _mergeticklists(ticks, self.getticks(min, max, self.ticklist[i], ticklevel = i))
461 for i in range(len(self.labellist)):
462 ticks = _mergeticklists(ticks, self.getticks(min, max, self.labellist[i], labellevel = i))
464 _mergelabels(ticks, self.labels)
466 return ticks
468 def lesspart(self):
469 "method is part of the implementation of _Ipart"
470 return None
472 def morepart(self):
473 "method is part of the implementation of _Ipart"
474 return None
477 class autolinpart:
478 """automatic linear partition scheme
479 - possible tick distances are explicitly provided to the constructor
480 - tick distances are adjusted to the axis range by multiplication or division by 10"""
482 __implements__ = _Ipart
484 defaultvariants = ((frac(1, 1), frac(1, 2)),
485 (frac(2, 1), frac(1, 1)),
486 (frac(5, 2), frac(5, 4)),
487 (frac(5, 1), frac(5, 2)))
489 def __init__(self, variants=defaultvariants, extendtick=0, epsilon=1e-10, mix=()):
490 """configuration of the partition scheme
491 - variants is a sequence of tickdist
492 - tickdist should be a sequence, where the first value
493 is the distance between ticks with ticklevel 0,
494 the second for ticklevel 1, etc.
495 - tickdist values must be frac instances or
496 strings convertable to fracs by the _ensurefrac function
497 - labellevel is set to None except for those ticks in the partitions,
498 where ticklevel is zero. There labellevel is also set to zero.
499 - extendtick allows for the extension of the range given to the
500 defaultpart method to include the next tick with the specified
501 level (None turns off this feature); note, that this feature is
502 also disabled, when an axis prohibits its range extension by
503 the extendmin/extendmax variables given to the defaultpart method
504 - epsilon allows for exceeding the axis range by this relative
505 value (relative to the axis range given to the defaultpart method)
506 without creating another tick specified by extendtick
507 - mix specifies another partition to be merged into the
508 created partition"""
509 self.variants = variants
510 self.extendtick = extendtick
511 self.epsilon = epsilon
512 self.mix = mix
514 def defaultpart(self, min, max, extendmin, extendmax):
515 "method is part of the implementation of _Ipart"
516 logmm = math.log(max - min) / math.log(10)
517 if logmm < 0: # correction for rounding towards zero of the int routine
518 base = frac(10L, 1, int(logmm - 1))
519 else:
520 base = frac(10L, 1, int(logmm))
521 ticks = map(_ensurefrac, self.variants[0])
522 useticks = [tick * base for tick in ticks]
523 self.lesstickindex = self.moretickindex = 0
524 self.lessbase = frac(base.enum, base.denom)
525 self.morebase = frac(base.enum, base.denom)
526 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
527 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
528 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
530 def lesspart(self):
531 "method is part of the implementation of _Ipart"
532 if self.lesstickindex < len(self.variants) - 1:
533 self.lesstickindex += 1
534 else:
535 self.lesstickindex = 0
536 self.lessbase.enum *= 10
537 ticks = map(_ensurefrac, self.variants[self.lesstickindex])
538 useticks = [tick * self.lessbase for tick in ticks]
539 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
540 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
542 def morepart(self):
543 "method is part of the implementation of _Ipart"
544 if self.moretickindex:
545 self.moretickindex -= 1
546 else:
547 self.moretickindex = len(self.variants) - 1
548 self.morebase.denom *= 10
549 ticks = map(_ensurefrac, self.variants[self.moretickindex])
550 useticks = [tick * self.morebase for tick in ticks]
551 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
552 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
555 class preexp:
556 """storage class for the definition of logarithmic axes partitions
557 instances of this class define tick positions suitable for
558 logarithmic axes by the following instance variables:
559 - exp: integer, which defines multiplicator (usually 10)
560 - pres: sequence of tick positions (rational numbers, e.g. instances of frac)
561 possible positions are these tick positions and arbitrary divisions
562 and multiplications by the exp value"""
564 def __init__(self, pres, exp):
565 "create a preexp instance and store its pres and exp information"
566 self.pres = helper.ensuresequence(pres)
567 self.exp = exp
570 class logpart(linpart):
571 """logarithmic partition scheme
572 ticks and label positions are explicitly provided to the constructor"""
574 __implements__ = _Ipart
576 pre1exp5 = preexp(frac(1, 1), 100000)
577 pre1exp4 = preexp(frac(1, 1), 10000)
578 pre1exp3 = preexp(frac(1, 1), 1000)
579 pre1exp2 = preexp(frac(1, 1), 100)
580 pre1exp = preexp(frac(1, 1), 10)
581 pre125exp = preexp((frac(1, 1), frac(2, 1), frac(5, 1)), 10)
582 pre1to9exp = preexp(map(lambda x: frac(x, 1), range(1, 10)), 10)
583 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
585 def __init__(self, tickpos=None, labelpos=None, labels=None, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
586 """configuration of the partition scheme
587 - tickpos and labelpos should be a sequence, where the first entry
588 is a preexp instance describing ticks with ticklevel/labellevel 0,
589 the second is a preexp instance for ticklevel/labellevel 1, etc.;
590 a single entry is allowed without being a sequence
591 - when labelpos is None and tickpos is not None, the tick entries
592 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
593 - labels are applied to the resulting partition via the
594 mergetexts function (additional information available there)
595 - extendtick allows for the extension of the range given to the
596 defaultpart method to include the next tick with the specified
597 level (None turns off this feature); note, that this feature is
598 also disabled, when an axis prohibits its range extension by
599 the extendmin/extendmax variables given to the defaultpart method
600 - extendlabel is analogous to extendtick, but for labels
601 - epsilon allows for exceeding the axis range by this relative
602 logarithm value (relative to the logarithm axis range given
603 to the defaultpart method) without creating another tick
604 specified by extendtick/extendlabel
605 - mix specifies another partition to be merged into the
606 created partition"""
607 if tickpos is None and labels is not None:
608 self.ticklist = (helper.ensuresequence(labelpos)[0],)
609 else:
610 self.ticklist = helper.ensuresequence(tickpos)
612 if labelpos is None and tickpos is not None:
613 self.labellist = (helper.ensuresequence(tickpos)[0],)
614 else:
615 self.labellist = helper.ensuresequence(labelpos)
616 self.labels = labels
617 self.extendtick = extendtick
618 self.extendlabel = extendlabel
619 self.epsilon = epsilon
620 self.mix = mix
622 def extendminmax(self, min, max, preexp, extendmin, extendmax):
623 """return new min, max tuple extending the range min, max
624 preexp describes the allowed tick positions
625 extendmin and extendmax are booleans to allow for the extension"""
626 minpower = None
627 maxpower = None
628 for i in xrange(len(preexp.pres)):
629 imin = int(math.floor(math.log(min / float(preexp.pres[i])) /
630 math.log(preexp.exp) + self.epsilon)) + 1
631 imax = int(math.ceil(math.log(max / float(preexp.pres[i])) /
632 math.log(preexp.exp) - self.epsilon)) - 1
633 if minpower is None or imin < minpower:
634 minpower, minindex = imin, i
635 if maxpower is None or imax >= maxpower:
636 maxpower, maxindex = imax, i
637 if minindex:
638 minfrac = preexp.pres[minindex - 1]
639 else:
640 minfrac = preexp.pres[-1]
641 minpower -= 1
642 if maxindex != len(preexp.pres) - 1:
643 maxfrac = preexp.pres[maxindex + 1]
644 else:
645 maxfrac = preexp.pres[0]
646 maxpower += 1
647 if extendmin:
648 min = float(minfrac) * float(preexp.exp) ** minpower
649 if extendmax:
650 max = float(maxfrac) * float(preexp.exp) ** maxpower
651 return min, max
653 def getticks(self, min, max, preexp, ticklevel=None, labellevel=None):
654 """return a list of ticks
655 - preexp describes the allowed tick positions
656 - the ticklevel of the ticks is set to ticklevel and
657 the labellevel is set to labellevel
658 - min, max is the range where ticks should be placed"""
659 ticks = list(self.mix)
660 minimin = 0
661 maximax = 0
662 for f in preexp.pres:
663 fracticks = []
664 imin = int(math.ceil(math.log(min / float(f)) /
665 math.log(preexp.exp) - 0.5 * self.epsilon))
666 imax = int(math.floor(math.log(max / float(f)) /
667 math.log(preexp.exp) + 0.5 * self.epsilon))
668 for i in range(imin, imax + 1):
669 pos = f * frac(preexp.exp, 1, i)
670 fracticks.append(tick(pos.enum, pos.denom, ticklevel = ticklevel, labellevel = labellevel))
671 ticks = _mergeticklists(ticks, fracticks)
672 return ticks
675 class autologpart(logpart):
676 """automatic logarithmic partition scheme
677 possible tick positions are explicitly provided to the constructor"""
679 __implements__ = _Ipart
681 defaultvariants = (((logpart.pre1exp, # ticks
682 logpart.pre1to9exp), # subticks
683 (logpart.pre1exp, # labels
684 logpart.pre125exp)), # sublevels
686 ((logpart.pre1exp, # ticks
687 logpart.pre1to9exp), # subticks
688 None), # labels like ticks
690 ((logpart.pre1exp2, # ticks
691 logpart.pre1exp), # subticks
692 None), # labels like ticks
694 ((logpart.pre1exp3, # ticks
695 logpart.pre1exp), # subticks
696 None), # labels like ticks
698 ((logpart.pre1exp4, # ticks
699 logpart.pre1exp), # subticks
700 None), # labels like ticks
702 ((logpart.pre1exp5, # ticks
703 logpart.pre1exp), # subticks
704 None)) # labels like ticks
706 def __init__(self, variants=defaultvariants, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
707 """configuration of the partition scheme
708 - variants should be a sequence of pairs of sequences of preexp
709 instances
710 - within each pair the first sequence contains preexp, where
711 the first preexp instance describes ticks positions with
712 ticklevel 0, the second preexp for ticklevel 1, etc.
713 - the second sequence within each pair describes the same as
714 before, but for labels
715 - within each pair: when the second entry (for the labels) is None
716 and the first entry (for the ticks) ticks is not None, the tick
717 entries for ticklevel 0 are used for labels and vice versa
718 (ticks<->labels)
719 - extendtick allows for the extension of the range given to the
720 defaultpart method to include the next tick with the specified
721 level (None turns off this feature); note, that this feature is
722 also disabled, when an axis prohibits its range extension by
723 the extendmin/extendmax variables given to the defaultpart method
724 - extendlabel is analogous to extendtick, but for labels
725 - epsilon allows for exceeding the axis range by this relative
726 logarithm value (relative to the logarithm axis range given
727 to the defaultpart method) without creating another tick
728 specified by extendtick/extendlabel
729 - mix specifies another partition to be merged into the
730 created partition"""
731 self.variants = variants
732 if len(variants) > 2:
733 self.variantsindex = divmod(len(variants), 2)[0]
734 else:
735 self.variantsindex = 0
736 self.extendtick = extendtick
737 self.extendlabel = extendlabel
738 self.epsilon = epsilon
739 self.mix = mix
741 def defaultpart(self, min, max, extendmin, extendmax):
742 "method is part of the implementation of _Ipart"
743 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
744 self.morevariantsindex = self.variantsindex
745 self.lessvariantsindex = self.variantsindex
746 part = logpart(tickpos=self.variants[self.variantsindex][0], labelpos=self.variants[self.variantsindex][1],
747 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
748 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
750 def lesspart(self):
751 "method is part of the implementation of _Ipart"
752 self.lessvariantsindex += 1
753 if self.lessvariantsindex < len(self.variants):
754 part = logpart(tickpos=self.variants[self.lessvariantsindex][0], labelpos=self.variants[self.lessvariantsindex][1],
755 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
756 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
758 def morepart(self):
759 "method is part of the implementation of _Ipart"
760 self.morevariantsindex -= 1
761 if self.morevariantsindex >= 0:
762 part = logpart(tickpos=self.variants[self.morevariantsindex][0], labelpos=self.variants[self.morevariantsindex][1],
763 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
764 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
768 ################################################################################
769 # rater
770 # conseptional remarks:
771 # - raters are used to calculate a rating for a realization of something
772 # - here, a rating means a positive floating point value
773 # - ratings are used to order those realizations by their suitability (lower
774 # ratings are better)
775 # - a rating of None means not suitable at all (those realizations should be
776 # thrown out)
777 ################################################################################
780 class cuberate:
781 """a cube rater
782 - a cube rater has an optimal value, where the rate becomes zero
783 - for a left (below the optimum) and a right value (above the optimum),
784 the rating is value is set to 1 (modified by an overall weight factor
785 for the rating)
786 - the analytic form of the rating is cubic for both, the left and
787 the right side of the rater, independently"""
789 # __implements__ = TODO (sole implementation)
791 def __init__(self, opt, left=None, right=None, weight=1):
792 """initializes the rater
793 - by default, left is set to zero, right is set to 3*opt
794 - left should be smaller than opt, right should be bigger than opt
795 - weight should be positive and is a factor multiplicated to the rates"""
796 if left is None:
797 left = 0
798 if right is None:
799 right = 3*opt
800 self.opt = opt
801 self.left = left
802 self.right = right
803 self.weight = weight
805 def rate(self, value, dense=1):
806 """returns a rating for a value
807 - the dense factor lineary rescales the rater (the optimum etc.),
808 e.g. a value bigger than one increases the optimum (when it is
809 positive) and a value lower than one decreases the optimum (when
810 it is positive); the dense factor itself should be positive"""
811 opt = self.opt * dense
812 if value < opt:
813 other = self.left * dense
814 elif value > opt:
815 other = self.right * dense
816 else:
817 return 0
818 factor = (value - opt) / float(other - opt)
819 return self.weight * (factor ** 3)
822 class distancerate:
823 """a distance rater (rates a list of distances)
824 - the distance rater rates a list of distances by rating each independently
825 and returning the average rate
826 - there is an optimal value, where the rate becomes zero
827 - the analytic form is linary for values above the optimal value
828 (twice the optimal value has the rating one, three times the optimal
829 value has the rating two, etc.)
830 - the analytic form is reciprocal subtracting one for values below the
831 optimal value (halve the optimal value has the rating one, one third of
832 the optimal value has the rating two, etc.)"""
834 # __implements__ = TODO (sole implementation)
836 def __init__(self, opt, weight=0.1):
837 """inititializes the rater
838 - opt is the optimal length (a visual PyX length)
839 - weight should be positive and is a factor multiplicated to the rates"""
840 self.opt_str = opt
841 self.weight = weight
843 def _rate(self, distances, dense=1):
844 """rate distances
845 - the distances are a sequence of positive floats in PostScript points
846 - the dense factor lineary rescales the rater (the optimum etc.),
847 e.g. a value bigger than one increases the optimum (when it is
848 positive) and a value lower than one decreases the optimum (when
849 it is positive); the dense factor itself should be positive"""
850 if len(distances):
851 opt = unit.topt(unit.length(self.opt_str, default_type="v")) / dense
852 rate = 0
853 for distance in distances:
854 if distance < opt:
855 rate += self.weight * (opt / distance - 1)
856 else:
857 rate += self.weight * (distance / opt - 1)
858 return rate / float(len(distances))
861 class axisrater:
862 """a rater for axis partitions
863 - the rating of axes is splited into two separate parts:
864 - rating of the partitions in terms of the number of ticks,
865 subticks, labels, etc.
866 - rating of the label distances
867 - in the end, a rate for an axis partition is the sum of these rates
868 - it is useful to first just rate the number of ticks etc.
869 and selecting those partitions, where this fits well -> as soon
870 as an complete rate (the sum of both parts from the list above)
871 of a first partition is below a rate of just the ticks of another
872 partition, this second partition will never be better than the
873 first one -> we gain speed by minimizing the number of partitions,
874 where label distances have to be taken into account)
875 - both parts of the rating are shifted into instances of raters
876 defined above --- right now, there is not yet a strict interface
877 for this delegation (should be done as soon as it is needed)"""
879 # __implements__ = TODO (sole implementation)
881 linticks = (cuberate(4), cuberate(10, weight=0.5), )
882 linlabels = (cuberate(4), )
883 logticks = (cuberate(5, right=20), cuberate(20, right=100, weight=0.5), )
884 loglabels = (cuberate(5, right=20), cuberate(5, left=-20, right=20, weight=0.5), )
885 stdtickrange = cuberate(1, weight=2)
886 stddistance = distancerate("1 cm")
888 def __init__(self, ticks=linticks, labels=linlabels, tickrange=stdtickrange, distance=stddistance):
889 """initializes the axis rater
890 - ticks and labels are lists of instances of cuberate
891 - the first entry in ticks rate the number of ticks, the
892 second the number of subticks, etc.; when there are no
893 ticks of a level or there is not rater for a level, the
894 level is just ignored
895 - labels is analogous, but for labels
896 - within the rating, all ticks with a higher level are
897 considered as ticks for a given level
898 - tickrange is a cuberate instance, which rates the covering
899 of an axis range by the ticks (as a relative value of the
900 tick range vs. the axis range), ticks might cover less or
901 more than the axis range (for the standard automatic axis
902 partition schemes an extention of the axis range is normal
903 and should get some penalty)
904 - distance is an distancerate instance"""
905 self.ticks = ticks
906 self.labels = labels
907 self.tickrange = tickrange
908 self.distance = distance
910 def ratepart(self, axis, part, dense=1):
911 """rates a partition by some global parameters
912 - takes into account the number of ticks, subticks, etc.,
913 number of labels, etc., and the coverage of the axis
914 range by the ticks
915 - when there are no ticks of a level or there was not rater
916 given in the constructor for a level, the level is just
917 ignored
918 - the method returns the sum of the rating results divided
919 by the sum of the weights of the raters
920 - within the rating, all ticks with a higher level are
921 considered as ticks for a given level"""
922 maxticklevel = maxlabellevel = 0
923 for tick in part:
924 if tick.ticklevel >= maxticklevel:
925 maxticklevel = tick.ticklevel + 1
926 if tick.labellevel >= maxlabellevel:
927 maxlabellevel = tick.labellevel + 1
928 ticks = [0]*maxticklevel
929 labels = [0]*maxlabellevel
930 if part is not None:
931 for tick in part:
932 if tick.ticklevel is not None:
933 for level in range(tick.ticklevel, maxticklevel):
934 ticks[level] += 1
935 if tick.labellevel is not None:
936 for level in range(tick.labellevel, maxlabellevel):
937 labels[level] += 1
938 rate = 0
939 weight = 0
940 for tick, rater in zip(ticks, self.ticks):
941 rate += rater.rate(tick, dense=dense)
942 weight += rater.weight
943 for label, rater in zip(labels, self.labels):
944 rate += rater.rate(label, dense=dense)
945 weight += rater.weight
946 if part is not None and len(part):
947 # XXX: tickrange was not yet applied
948 rate += self.tickrange.rate(axis.convert(float(part[-1]) * axis.divisor) -
949 axis.convert(float(part[0]) * axis.divisor))
950 else:
951 rate += self.tickrange.rate(0)
952 weight += self.tickrange.weight
953 return rate/weight
955 def ratelayout(self, axiscanvas, dense=1):
956 # TODO: update docstring
957 """rate distances
958 - the distances should be collected as box distances of
959 subsequent labels (of any level))
960 - the distances are a sequence of positive floats in
961 PostScript points
962 - the dense factor is used within the distancerate instance"""
963 if len(axiscanvas.labels) > 1:
964 try:
965 distances = [axiscanvas.labels[i]._boxdistance(axiscanvas.labels[i+1]) for i in range(len(axiscanvas.labels) - 1)]
966 except box.BoxCrossError:
967 return None
968 return self.distance._rate(distances, dense)
969 else:
970 return 0
973 ################################################################################
974 # texter
975 # texter automatically create labels for tick instances
976 ################################################################################
979 class _Itexter:
981 def labels(self, ticks):
982 """fill the label attribute of ticks
983 - ticks is a list of instances of tick
984 - for each element of ticks the value of the attribute label is set to
985 a string appropriate to the attributes enum and denom of that tick
986 instance
987 - label attributes of the tick instances are just kept, whenever they
988 are not equal to None
989 - the method might extend the labelattrs attribute of the ticks"""
992 class rationaltexter:
993 "a texter creating rational labels (e.g. 'a/b' or even 'a \over b')"
994 # XXX: we use divmod here to be more expicit
996 __implements__ = _Itexter
998 def __init__(self, prefix="", infix="", suffix="",
999 enumprefix="", enuminfix="", enumsuffix="",
1000 denomprefix="", denominfix="", denomsuffix="",
1001 minus="-", minuspos=0, over=r"{{%s}\over{%s}}",
1002 equaldenom=0, skip1=1, skipenum0=1, skipenum1=1, skipdenom1=1,
1003 labelattrs=textmodule.mathmode):
1004 r"""initializes the instance
1005 - prefix, infix, and suffix (strings) are added at the begin,
1006 immediately after the minus, and at the end of the label,
1007 respectively
1008 - prefixenum, infixenum, and suffixenum (strings) are added
1009 to the labels enumerator correspondingly
1010 - prefixdenom, infixdenom, and suffixdenom (strings) are added
1011 to the labels denominator correspondingly
1012 - minus (string) is inserted for negative numbers
1013 - minuspos is an integer, which determines the position, where the
1014 minus sign has to be placed; the following values are allowed:
1015 1 - writes the minus in front of the enumerator
1016 0 - writes the minus in front of the hole fraction
1017 -1 - writes the minus in front of the denominator
1018 - over (string) is taken as a format string generating the
1019 fraction bar; it has to contain exactly two string insert
1020 operators "%s" -- the first for the enumerator and the second
1021 for the denominator; by far the most common examples are
1022 r"{{%s}\over{%s}}" and "{{%s}/{%s}}"
1023 - usually the enumerator and denominator are canceled; however,
1024 when equaldenom is set, the least common multiple of all
1025 denominators is used
1026 - skip1 (boolean) just prints the prefix, the minus (if present),
1027 the infix and the suffix, when the value is plus or minus one
1028 and at least one of prefix, infix and the suffix is present
1029 - skipenum0 (boolean) just prints a zero instead of
1030 the hole fraction, when the enumerator is zero;
1031 no prefixes, infixes, and suffixes are taken into account
1032 - skipenum1 (boolean) just prints the enumprefix, the minus (if present),
1033 the enuminfix and the enumsuffix, when the enum value is plus or minus one
1034 and at least one of enumprefix, enuminfix and the enumsuffix is present
1035 - skipdenom1 (boolean) just prints the enumerator instead of
1036 the hole fraction, when the denominator is one and none of the parameters
1037 denomprefix, denominfix and denomsuffix are set and minuspos is not -1 or the
1038 fraction is positive
1039 - labelattrs is a sequence of attributes for a texrunners text method;
1040 a single is allowed without being a sequence; None is considered as
1041 an empty sequence"""
1042 self.prefix = prefix
1043 self.infix = infix
1044 self.suffix = suffix
1045 self.enumprefix = enumprefix
1046 self.enuminfix = enuminfix
1047 self.enumsuffix = enumsuffix
1048 self.denomprefix = denomprefix
1049 self.denominfix = denominfix
1050 self.denomsuffix = denomsuffix
1051 self.minus = minus
1052 self.minuspos = minuspos
1053 self.over = over
1054 self.equaldenom = equaldenom
1055 self.skip1 = skip1
1056 self.skipenum0 = skipenum0
1057 self.skipenum1 = skipenum1
1058 self.skipdenom1 = skipdenom1
1059 self.labelattrs = helper.ensurelist(labelattrs)
1061 def gcd(self, *n):
1062 """returns the greates common divisor of all elements in n
1063 - the elements of n must be non-negative integers
1064 - return None if the number of elements is zero
1065 - the greates common divisor is not affected when some
1066 of the elements are zero, but it becomes zero when
1067 all elements are zero"""
1068 if len(n) == 2:
1069 i, j = n
1070 if i < j:
1071 i, j = j, i
1072 while j > 0:
1073 i, (dummy, j) = j, divmod(i, j)
1074 return i
1075 if len(n):
1076 res = n[0]
1077 for i in n[1:]:
1078 res = self.gcd(res, i)
1079 return res
1081 def lcm(self, *n):
1082 """returns the least common multiple of all elements in n
1083 - the elements of n must be non-negative integers
1084 - return None if the number of elements is zero
1085 - the least common multiple is zero when some of the
1086 elements are zero"""
1087 if len(n):
1088 res = n[0]
1089 for i in n[1:]:
1090 res = divmod(res * i, self.gcd(res, i))[0]
1091 return res
1093 def labels(self, ticks):
1094 labeledticks = []
1095 for tick in ticks:
1096 if tick.label is None and tick.labellevel is not None:
1097 labeledticks.append(tick)
1098 tick.temp_fracenum = tick.enum
1099 tick.temp_fracdenom = tick.denom
1100 tick.temp_fracminus = 1
1101 if tick.temp_fracenum < 0:
1102 tick.temp_fracminus *= -1
1103 tick.temp_fracenum *= -1
1104 if tick.temp_fracdenom < 0:
1105 tick.temp_fracminus *= -1
1106 tick.temp_fracdenom *= -1
1107 gcd = self.gcd(tick.temp_fracenum, tick.temp_fracdenom)
1108 (tick.temp_fracenum, dummy1), (tick.temp_fracdenom, dummy2) = divmod(tick.enum, gcd), divmod(tick.temp_fracdenom, gcd)
1109 if self.equaldenom:
1110 equaldenom = self.lcm(*[tick.temp_fracdenom for tick in ticks if tick.label is None])
1111 if equaldenom is not None:
1112 for tick in labeledticks:
1113 factor, dummy = divmod(equaldenom, tick.temp_fracdenom)
1114 tick.temp_fracenum, tick.temp_fracdenom = factor * tick.temp_fracenum, factor * tick.temp_fracdenom
1115 for tick in labeledticks:
1116 fracminus = ""
1117 fracenumminus = ""
1118 fracdenomminus = ""
1119 if tick.temp_fracminus == -1:
1120 if self.minuspos == 0:
1121 fracminus = self.minus
1122 elif self.minuspos == 1:
1123 fracenumminus = self.minus
1124 elif self.minuspos == -1:
1125 fracdenomminus = self.minus
1126 else:
1127 raise RuntimeError("invalid minuspos")
1128 else:
1129 tick.temp_fracminus = ""
1130 if self.skipenum0 and tick.temp_fracenum == 0:
1131 tick.label = "0"
1132 elif (self.skip1 and self.skipdenom1 and tick.temp_fracenum == 1 and tick.temp_fracdenom == 1 and
1133 (len(self.prefix) or len(self.infix) or len(self.suffix)) and
1134 not len(fracenumminus) and not len(self.enumprefix) and not len(self.enuminfix) and not len(self.enumsuffix) and
1135 not len(fracdenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix)):
1136 tick.label = "%s%s%s%s" % (self.prefix, fracminus, self.infix, self.suffix)
1137 else:
1138 if self.skipenum1 and tick.temp_fracenum == 1 and (len(self.enumprefix) or len(self.enuminfix) or len(self.enumsuffix)):
1139 tick.temp_fracenum = "%s%s%s%s" % (self.enumprefix, fracenumminus, self.enuminfix, self.enumsuffix)
1140 else:
1141 tick.temp_fracenum = "%s%s%s%i%s" % (self.enumprefix, fracenumminus, self.enuminfix, tick.temp_fracenum, self.enumsuffix)
1142 if self.skipdenom1 and tick.temp_fracdenom == 1 and not len(fracdenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix):
1143 frac = tick.temp_fracenum
1144 else:
1145 tick.temp_fracdenom = "%s%s%s%i%s" % (self.denomprefix, fracdenomminus, self.denominfix, tick.temp_fracdenom, self.denomsuffix)
1146 frac = self.over % (tick.temp_fracenum, tick.temp_fracdenom)
1147 tick.label = "%s%s%s%s%s" % (self.prefix, fracminus, self.infix, frac, self.suffix)
1148 # del tick.temp_fracenum # we've inserted those temporary variables ... and do not care any longer about them
1149 # del tick.temp_fracdenom
1150 # del tick.temp_fracminus
1151 tick.labelattrs.extend(self.labelattrs)
1154 class decimaltexter:
1155 "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')"
1157 __implements__ = _Itexter
1159 def __init__(self, prefix="", infix="", suffix="", equalprecision=0,
1160 decimalsep=".", thousandsep="", thousandthpartsep="",
1161 minus="-", period=r"\overline{%s}", labelattrs=textmodule.mathmode):
1162 r"""initializes the instance
1163 - prefix, infix, and suffix (strings) are added at the begin,
1164 immediately after the minus, and at the end of the label,
1165 respectively
1166 - decimalsep, thousandsep, and thousandthpartsep (strings)
1167 are used as separators
1168 - minus (string) is inserted for negative numbers
1169 - period (string) is taken as a format string generating a period;
1170 it has to contain exactly one string insert operators "%s" for the
1171 period; usually it should be r"\overline{%s}"
1172 - labelattrs is a sequence of attributes for a texrunners text method;
1173 a single is allowed without being a sequence; None is considered as
1174 an empty sequence"""
1175 self.prefix = prefix
1176 self.infix = infix
1177 self.suffix = suffix
1178 self.equalprecision = equalprecision
1179 self.decimalsep = decimalsep
1180 self.thousandsep = thousandsep
1181 self.thousandthpartsep = thousandthpartsep
1182 self.minus = minus
1183 self.period = period
1184 self.labelattrs = helper.ensurelist(labelattrs)
1186 def labels(self, ticks):
1187 labeledticks = []
1188 maxdecprecision = 0
1189 for tick in ticks:
1190 if tick.label is None and tick.labellevel is not None:
1191 labeledticks.append(tick)
1192 m, n = tick.enum, tick.denom
1193 if m < 0: m *= -1
1194 if n < 0: n *= -1
1195 whole, reminder = divmod(m, n)
1196 whole = str(whole)
1197 if len(self.thousandsep):
1198 l = len(whole)
1199 tick.label = ""
1200 for i in range(l):
1201 tick.label += whole[i]
1202 if not ((l-i-1) % 3) and l > i+1:
1203 tick.label += self.thousandsep
1204 else:
1205 tick.label = whole
1206 if reminder:
1207 tick.label += self.decimalsep
1208 oldreminders = []
1209 tick.temp_decprecision = 0
1210 while (reminder):
1211 tick.temp_decprecision += 1
1212 if reminder in oldreminders:
1213 tick.temp_decprecision = None
1214 periodstart = len(tick.label) - (len(oldreminders) - oldreminders.index(reminder))
1215 tick.label = tick.label[:periodstart] + self.period % tick.label[periodstart:]
1216 break
1217 oldreminders += [reminder]
1218 reminder *= 10
1219 whole, reminder = divmod(reminder, n)
1220 if not ((tick.temp_decprecision - 1) % 3) and tick.temp_decprecision > 1:
1221 tick.label += self.thousandthpartsep
1222 tick.label += str(whole)
1223 if maxdecprecision < tick.temp_decprecision:
1224 maxdecprecision = tick.temp_decprecision
1225 if self.equalprecision:
1226 for tick in labeledticks:
1227 if tick.temp_decprecision is not None:
1228 if tick.temp_decprecision == 0 and maxdecprecision > 0:
1229 tick.label += self.decimalsep
1230 for i in range(tick.temp_decprecision, maxdecprecision):
1231 if not ((i - 1) % 3) and i > 1:
1232 tick.label += self.thousandthpartsep
1233 tick.label += "0"
1234 for tick in labeledticks:
1235 if tick.enum * tick.denom < 0:
1236 minus = self.minus
1237 else:
1238 minus = ""
1239 tick.label = "%s%s%s%s%s" % (self.prefix, minus, self.infix, tick.label, self.suffix)
1240 tick.labelattrs.extend(self.labelattrs)
1241 # del tick.temp_decprecision # we've inserted this temporary variable ... and do not care any longer about it
1244 class exponentialtexter:
1245 "a texter creating labels with exponentials (e.g. '2\cdot10^5')"
1247 __implements__ = _Itexter
1249 def __init__(self, plus="", minus="-",
1250 mantissaexp=r"{{%s}\cdot10^{%s}}",
1251 nomantissaexp=r"{10^{%s}}",
1252 minusnomantissaexp=r"{-10^{%s}}",
1253 mantissamin=frac(1, 1), mantissamax=frac(10, 1),
1254 skipmantissa1=0, skipallmantissa1=1,
1255 mantissatexter=decimaltexter()):
1256 r"""initializes the instance
1257 - plus or minus (string) is inserted for positive or negative exponents
1258 - mantissaexp (string) is taken as a format string generating the exponent;
1259 it has to contain exactly two string insert operators "%s" --
1260 the first for the mantissa and the second for the exponent;
1261 examples are r"{{%s}\cdot10^{%s}}" and r"{{%s}{\rm e}^{%s}}"
1262 - nomantissaexp (string) is taken as a format string generating the exponent
1263 when the mantissa is one and should be skipped; it has to contain
1264 exactly one string insert operators "%s" for the exponent;
1265 an examples is r"{10^{%s}}"
1266 - minusnomantissaexp (string) is taken as a format string generating the exponent
1267 when the mantissa is minus one and should be skipped; it has to contain
1268 exactly one string insert operators "%s" for the exponent; might be set to None
1269 to disallow skipping of any mantissa minus one
1270 an examples is r"{-10^{%s}}"
1271 - mantissamin and mantissamax are the minimum and maximum of the mantissa;
1272 they are frac instances greater than zero and mantissamin < mantissamax;
1273 the sign of the tick is ignored here
1274 - skipmantissa1 (boolean) turns on skipping of any mantissa equals one
1275 (and minus when minusnomantissaexp is set)
1276 - skipallmantissa1 (boolean) as above, but all mantissas must be 1
1277 - mantissatexter is the texter for the mantissa"""
1278 self.plus = plus
1279 self.minus = minus
1280 self.mantissaexp = mantissaexp
1281 self.nomantissaexp = nomantissaexp
1282 self.minusnomantissaexp = minusnomantissaexp
1283 self.mantissamin = mantissamin
1284 self.mantissamax = mantissamax
1285 self.mantissamindivmax = self.mantissamin / self.mantissamax
1286 self.mantissamaxdivmin = self.mantissamax / self.mantissamin
1287 self.skipmantissa1 = skipmantissa1
1288 self.skipallmantissa1 = skipallmantissa1
1289 self.mantissatexter = mantissatexter
1291 def labels(self, ticks):
1292 labeledticks = []
1293 for tick in ticks:
1294 if tick.label is None and tick.labellevel is not None:
1295 tick.temp_orgenum, tick.temp_orgdenom = tick.enum, tick.denom
1296 labeledticks.append(tick)
1297 tick.temp_exp = 0
1298 if tick.enum:
1299 while abs(tick) >= self.mantissamax:
1300 tick.temp_exp += 1
1301 x = tick * self.mantissamindivmax
1302 tick.enum, tick.denom = x.enum, x.denom
1303 while abs(tick) < self.mantissamin:
1304 tick.temp_exp -= 1
1305 x = tick * self.mantissamaxdivmin
1306 tick.enum, tick.denom = x.enum, x.denom
1307 if tick.temp_exp < 0:
1308 tick.temp_exp = "%s%i" % (self.minus, -tick.temp_exp)
1309 else:
1310 tick.temp_exp = "%s%i" % (self.plus, tick.temp_exp)
1311 self.mantissatexter.labels(labeledticks)
1312 if self.minusnomantissaexp is not None:
1313 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if abs(tick.enum) == abs(tick.denom)])
1314 else:
1315 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if tick.enum == tick.denom])
1316 for tick in labeledticks:
1317 if (self.skipallmantissa1 and allmantissa1 or
1318 (self.skipmantissa1 and (tick.enum == tick.denom or
1319 (tick.enum == -tick.denom and self.minusnomantissaexp is not None)))):
1320 if tick.enum == tick.denom:
1321 tick.label = self.nomantissaexp % tick.temp_exp
1322 else:
1323 tick.label = self.minusnomantissaexp % tick.temp_exp
1324 else:
1325 tick.label = self.mantissaexp % (tick.label, tick.temp_exp)
1326 tick.enum, tick.denom = tick.temp_orgenum, tick.temp_orgdenom
1327 # del tick.temp_orgenum # we've inserted those temporary variables ... and do not care any longer about them
1328 # del tick.temp_orgdenom
1329 # del tick.temp_exp
1332 class defaulttexter:
1333 "a texter creating decimal or exponential labels"
1335 __implements__ = _Itexter
1337 def __init__(self, smallestdecimal=frac(1, 1000),
1338 biggestdecimal=frac(9999, 1),
1339 equaldecision=1,
1340 decimaltexter=decimaltexter(),
1341 exponentialtexter=exponentialtexter()):
1342 r"""initializes the instance
1343 - smallestdecimal and biggestdecimal are the smallest and
1344 biggest decimal values, where the decimaltexter should be used;
1345 they are frac instances; the sign of the tick is ignored here;
1346 a tick at zero is considered for the decimaltexter as well
1347 - equaldecision (boolean) uses decimaltexter or exponentialtexter
1348 globaly (set) or for each tick separately (unset)
1349 - decimaltexter and exponentialtexter are texters to be used"""
1350 self.smallestdecimal = smallestdecimal
1351 self.biggestdecimal = biggestdecimal
1352 self.equaldecision = equaldecision
1353 self.decimaltexter = decimaltexter
1354 self.exponentialtexter= exponentialtexter
1356 def labels(self, ticks):
1357 decticks = []
1358 expticks = []
1359 for tick in ticks:
1360 if tick.label is None and tick.labellevel is not None:
1361 if not tick.enum or (abs(tick) >= self.smallestdecimal and abs(tick) <= self.biggestdecimal):
1362 decticks.append(tick)
1363 else:
1364 expticks.append(tick)
1365 if self.equaldecision:
1366 if len(expticks):
1367 self.exponentialtexter.labels(ticks)
1368 else:
1369 self.decimaltexter.labels(ticks)
1370 else:
1371 for tick in decticks:
1372 self.decimaltexter.labels([tick])
1373 for tick in expticks:
1374 self.exponentialtexter.labels([tick])
1377 ################################################################################
1378 # axis painter
1379 ################################################################################
1382 class axiscanvas(canvas.canvas):
1383 """axis canvas
1384 - an axis canvas is a regular canvas to be filled by
1385 a axispainters painter method
1386 - it contains _extent (a float in postscript points) to be used
1387 for the alignment of additional axes; the axis extent should be
1388 filled by the axispainters painter method
1389 - it contains labels (a list of textboxes) to be used to rate the
1390 distances between the labels if needed by the axis later on;
1391 the painter method has not only to insert the labels into this
1392 canvas, but should also fill this list"""
1394 # __implements__ = TODO (sole implementation)
1396 def __init__(self, *args, **kwargs):
1397 """initializes the instance
1398 - sets _extent to zero
1399 - sets labels to an empty list"""
1400 canvas.canvas.__init__(self, *args, **kwargs)
1401 self._extent = 0
1402 self.labels = []
1405 class rotatetext:
1406 """create rotations accordingly to tick directions
1407 - upsidedown rotations are suppressed by rotating them by another 180 degree"""
1409 # __implements__ = TODO (sole implementation)
1411 def __init__(self, direction, epsilon=1e-10):
1412 """initializes the instance
1413 - direction is an angle to be used relative to the tick direction
1414 - epsilon is the value by which 90 degrees can be exceeded before
1415 an 180 degree rotation is added"""
1416 self.direction = direction
1417 self.epsilon = epsilon
1419 def trafo(self, dx, dy):
1420 """returns a rotation transformation accordingly to the tick direction
1421 - dx and dy are the direction of the tick"""
1422 direction = self.direction + math.atan2(dy, dx) * 180 / math.pi
1423 while (direction > 90 + self.epsilon):
1424 direction -= 180
1425 while (direction < -90 - self.epsilon):
1426 direction += 180
1427 return trafomodule.rotate(direction)
1430 paralleltext = rotatetext(-90)
1431 orthogonaltext = rotatetext(0)
1434 class _Iaxispainter:
1435 "class for painting axes"
1437 def paint(self, graph, axis, part, axiscanvas):
1438 """paint ticks into the axiscanvas
1439 - graph, axis, and ticks should not be modified (we may
1440 add some temporary variables like part[i].temp_xxx,
1441 which might be used just temporary) -- the idea is that
1442 all things can be used several times
1443 - also do not modify the instance (self) -- even this
1444 instance might be used several times; thus do not modify
1445 attributes like self.titleattrs etc. (just use a copy of it)
1446 - the graphs texrunner should be used to create textboxes
1447 (beside that, there should be no reason to access the
1448 graph directly --- please correct this statement when
1449 different design decisions will take place later on)
1450 - the axis should be accessed to get the tick positions:
1451 for that the following methods are used:
1452 - _vtickpoint
1453 - vtickdirection
1454 - convert, divisor?!
1455 - vgridpath
1456 - vbaseline
1457 see documentation of an axis interface for a detailed
1458 description of those methods
1459 - the method might access some additional attributes from
1460 the axis, e.g. the axis title -- the axis painter should
1461 document this behavior and rely on the availability of
1462 those attributes -> it becomes a question of the proper
1463 usage of the combination of axis & axispainter
1464 - the ticks are a list of tick instances; there type must
1465 be suitable for the tick position methods of the axis ->
1466 it should again be a question ov the proper usage of the
1467 combination of axis & axispainter
1468 - the axiscanvas is a axiscanvas instance and should be
1469 filled with the ticks and labels; note that the _extent
1470 and labels instance variables should be filled as
1471 documented in the axiscanvas"""
1474 class axistitlepainter:
1475 """class for painting an axis title
1476 - the axis must have a title attribute when using this painter;
1477 this title might be None"""
1479 __implements__ = _Iaxispainter
1481 def __init__(self, titledist="0.3 cm",
1482 titleattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1483 titledirection=paralleltext,
1484 titlepos=0.5):
1485 """initialized the instance
1486 - titledist is a visual PyX length giving the distance
1487 of the title from the axis extent already there (a title might
1488 be added after labels or other things are plotted already)
1489 - labelattrs is a sequence of attributes for a texrunners text
1490 method; a single is allowed without being a sequence; None
1491 turns off the title
1492 - titledirection is an instance of rotatetext or None
1493 - titlepos is the position of the title in graph coordinates"""
1494 self.titledist_str = titledist
1495 self.titleattrs = titleattrs
1496 self.titledirection = titledirection
1497 self.titlepos = titlepos
1499 def paint(self, graph, axis, part, axiscanvas):
1500 if axis.title is not None and self.titleattrs is not None:
1501 titledist = unit.topt(unit.length(self.titledist_str, default_type="v"))
1502 x, y = axis._vtickpoint(axis, self.titlepos)
1503 dx, dy = axis.vtickdirection(axis, self.titlepos)
1504 titleattrs = helper.ensurelist(self.titleattrs)
1505 if self.titledirection is not None:
1506 titleattrs.append(self.titledirection.trafo(dx, dy))
1507 title = graph.texrunner._text(x, y, axis.title, *titleattrs)
1508 axiscanvas._extent += titledist
1509 title._linealign(axiscanvas._extent, dx, dy)
1510 axiscanvas._extent += title._extent(dx, dy)
1511 axiscanvas.insert(title)
1514 class axispainter(axistitlepainter):
1515 """class for painting the ticks and labels of an axis
1516 - the inherited titleaxispainter is used to paint the title of
1517 the axis as well
1518 - note that the type of the elements of ticks given as an argument
1519 of the paint method must be suitable for the tick position methods
1520 of the axis"""
1522 __implements__ = _Iaxispainter
1524 defaultticklengths = ["%0.5f cm" % (0.2*goldenmean**(-i)) for i in range(10)]
1526 def __init__(self, innerticklengths=defaultticklengths,
1527 outerticklengths=None,
1528 tickattrs=(),
1529 gridattrs=None,
1530 zerolineattrs=(),
1531 baselineattrs=canvas.linecap.square,
1532 labeldist="0.3 cm",
1533 labelattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1534 labeldirection=None,
1535 labelhequalize=0,
1536 labelvequalize=1,
1537 **kwargs):
1538 """initializes the instance
1539 - innerticklenths and outerticklengths are two sequences of
1540 visual PyX lengths for ticks, subticks, etc. plotted inside
1541 and outside of the graph; when a single value is given, it
1542 is used for all tick levels; None turns off ticks inside or
1543 outside of the graph
1544 - tickattrs are a sequence of stroke attributes for the ticks;
1545 a single entry is allowed without being a sequence; None turns
1546 off ticks
1547 - gridlineattrs are a sequence of sequences used as stroke
1548 attributes for ticks, subticks etc.; when a single sequence
1549 is given, it is used for ticks, subticks, etc.; a single
1550 entry is allowed without being a sequence; None turns off
1551 gridlines
1552 - zerolineattrs are a sequence of stroke attributes for a grid
1553 line at axis value zero; a single entry is allowed without
1554 being a sequence; None turns off the zeroline
1555 - baselineattrs are a sequence of stroke attributes for a grid
1556 line at axis value zero; a single entry is allowed without
1557 being a sequence; None turns off the baseline
1558 - labeldist is a visual PyX length for the distance of the labels
1559 from the axis baseline
1560 - labelattrs is a sequence of attributes for a texrunners text
1561 method; a single entry is allowed without being a sequence;
1562 None turns off the labels
1563 - titledirection is an instance of rotatetext or None
1564 - labelhequalize and labelvequalize (booleans) perform an equal
1565 alignment for straight vertical and horizontal axes, respectively
1566 - futher keyword arguments are passed to axistitlepainter"""
1567 self.innerticklengths_str = innerticklengths
1568 self.outerticklengths_str = outerticklengths
1569 self.tickattrs = tickattrs
1570 self.gridattrs = gridattrs
1571 self.zerolineattrs = zerolineattrs
1572 self.baselineattrs = baselineattrs
1573 self.labeldist_str = labeldist
1574 self.labelattrs = labelattrs
1575 self.labeldirection = labeldirection
1576 self.labelhequalize = labelhequalize
1577 self.labelvequalize = labelvequalize
1578 axistitlepainter.__init__(self, **kwargs)
1580 def paint(self, graph, axis, part, axiscanvas):
1581 labeldist = unit.topt(unit.length(self.labeldist_str, default_type="v"))
1582 for tick in part:
1583 tick.temp_v = axis.convert(float(tick) * axis.divisor)
1584 tick.temp_x, tick.temp_y = axis._vtickpoint(axis, tick.temp_v)
1585 tick.temp_dx, tick.temp_dy = axis.vtickdirection(axis, tick.temp_v)
1587 # create & align tick.temp_labelbox
1588 for tick in part:
1589 if tick.labellevel is not None:
1590 labelattrs = helper.getsequenceno(self.labelattrs, tick.labellevel)
1591 if labelattrs is not None:
1592 labelattrs = list(labelattrs[:])
1593 if self.labeldirection is not None:
1594 labelattrs.append(self.labeldirection.trafo(dx, dy))
1595 if tick.labelattrs is not None:
1596 labelattrs.extend(helper.ensurelist(tick.labelattrs))
1597 tick.temp_labelbox = graph.texrunner._text(tick.temp_x, tick.temp_y, tick.label, *labelattrs)
1598 if len(part) > 1:
1599 equaldirection = 1
1600 for tick in part[1:]:
1601 if tick.temp_dx != part[0].temp_dx or tick.temp_dy != part[0].temp_dy:
1602 equaldirection = 0
1603 else:
1604 equaldirection = 0
1605 if equaldirection and ((not part[0].temp_dx and self.labelvequalize) or
1606 (not part[0].temp_dy and self.labelhequalize)):
1607 if self.labelattrs is not None:
1608 box._linealignequal([tick.temp_labelbox for tick in part if tick.labellevel is not None],
1609 labeldist, part[0].temp_dx, part[0].temp_dy)
1610 else:
1611 for tick in part:
1612 if tick.labellevel is not None and self.labelattrs is not None:
1613 tick.temp_labelbox._linealign(labeldist, tick.temp_dx, tick.temp_dy)
1615 def topt(arg):
1616 if helper.issequence(arg):
1617 return [unit.topt(unit.length(a, default_type="v")) for a in arg]
1618 if arg is not None:
1619 return unit.topt(unit.length(arg, default_type="v"))
1620 innerticklengths = topt(self.innerticklengths_str)
1621 outerticklengths = topt(self.outerticklengths_str)
1623 for tick in part:
1624 if tick.ticklevel is not None:
1625 innerticklength = helper.getitemno(innerticklengths, tick.ticklevel)
1626 outerticklength = helper.getitemno(outerticklengths, tick.ticklevel)
1627 if not (innerticklength is None and outerticklength is None):
1628 if innerticklength is None:
1629 innerticklength = 0
1630 if outerticklength is None:
1631 outerticklength = 0
1632 tickattrs = helper.getsequenceno(self.tickattrs, tick.ticklevel)
1633 if tickattrs is not None:
1634 x1 = tick.temp_x - tick.temp_dx * innerticklength
1635 y1 = tick.temp_y - tick.temp_dy * innerticklength
1636 x2 = tick.temp_x + tick.temp_dx * outerticklength
1637 y2 = tick.temp_y + tick.temp_dy * outerticklength
1638 axiscanvas.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(tickattrs))
1639 if tick != frac(0, 1) or self.zerolineattrs is None:
1640 gridattrs = helper.getsequenceno(self.gridattrs, tick.ticklevel)
1641 if gridattrs is not None:
1642 axiscanvas.stroke(axis.vgridpath(tick.temp_v), *helper.ensuresequence(gridattrs))
1643 if outerticklength > axiscanvas._extent:
1644 axiscanvas._extent = outerticklength
1645 if -innerticklength > axiscanvas._extent:
1646 axiscanvas._extent = -innerticklength
1647 if tick.labellevel is not None and self.labelattrs is not None:
1648 axiscanvas.insert(tick.temp_labelbox)
1649 axiscanvas.labels.append(tick.temp_labelbox)
1650 extent = tick.temp_labelbox._extent(tick.temp_dx, tick.temp_dy) + labeldist
1651 if extent > axiscanvas._extent:
1652 axiscanvas._extent = extent
1653 if self.baselineattrs is not None:
1654 axiscanvas.stroke(axis.vbaseline(axis), *helper.ensuresequence(self.baselineattrs))
1655 if self.zerolineattrs is not None:
1656 if len(part) and part[0] * part[-1] < frac(0, 1):
1657 axiscanvas.stroke(axis.vgridpath(axis.convert(0)), *helper.ensuresequence(self.zerolineattrs))
1659 # for tick in part:
1660 # del tick.temp_v # we've inserted those temporary variables ... and do not care any longer about them
1661 # del tick.temp_x
1662 # del tick.temp_y
1663 # del tick.temp_dx
1664 # del tick.temp_dy
1665 # if tick.labellevel is not None and self.labelattrs is not None:
1666 # del tick.temp_labelbox
1668 axistitlepainter.paint(self, graph, axis, part, axiscanvas)
1671 class linkaxispainter(axispainter):
1672 """class for painting a linked axis
1673 - the inherited axispainter is used to paint the axis
1674 - modifies some constructor defaults"""
1676 __implements__ = _Iaxispainter
1678 def __init__(self, gridattrs=None,
1679 zerolineattrs=None,
1680 labelattrs=None,
1681 **kwargs):
1682 """initializes the instance
1683 - the gridattrs default is set to None thus skipping the girdlines
1684 - the zerolineattrs default is set to None thus skipping the zeroline
1685 - the labelattrs default is set to None thus skipping the labels
1686 - all keyword arguments are passed to axispainter"""
1687 axispainter.__init__(self, gridattrs=gridattrs,
1688 zerolineattrs=zerolineattrs,
1689 labelattrs=labelattrs,
1690 **kwargs)
1692 # def paint(self,*xxx): pass
1695 class splitaxispainter(axistitlepainter): pass
1696 # class splitaxispainter(axistitlepainter):
1698 # def __init__(self, breaklinesdist=0.05,
1699 # breaklineslength=0.5,
1700 # breaklinesangle=-60,
1701 # breaklinesattrs=(),
1702 # **args):
1703 # self.breaklinesdist_str = breaklinesdist
1704 # self.breaklineslength_str = breaklineslength
1705 # self.breaklinesangle = breaklinesangle
1706 # self.breaklinesattrs = breaklinesattrs
1707 # axistitlepainter.__init__(self, **args)
1709 # def subvbaseline(self, axis, v1=None, v2=None):
1710 # if v1 is None:
1711 # if self.breaklinesattrs is None:
1712 # left = axis.vmin
1713 # else:
1714 # if axis.vminover is None:
1715 # left = None
1716 # else:
1717 # left = axis.vminover
1718 # else:
1719 # left = axis.vmin+v1*(axis.vmax-axis.vmin)
1720 # if v2 is None:
1721 # if self.breaklinesattrs is None:
1722 # right = axis.vmax
1723 # else:
1724 # if axis.vmaxover is None:
1725 # right = None
1726 # else:
1727 # right = axis.vmaxover
1728 # else:
1729 # right = axis.vmin+v2*(axis.vmax-axis.vmin)
1730 # return axis.baseaxis.vbaseline(axis.baseaxis, left, right)
1732 # def dolayout(self, graph, axis):
1733 # if self.breaklinesattrs is not None:
1734 # self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1735 # self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1736 # self._breaklinesdist = unit.topt(self.breaklinesdist)
1737 # self._breaklineslength = unit.topt(self.breaklineslength)
1738 # self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1739 # self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1740 # axis.layoutdata._extent = (math.fabs(0.5 * self._breaklinesdist * self.cos) +
1741 # math.fabs(0.5 * self._breaklineslength * self.sin))
1742 # else:
1743 # axis.layoutdata._extent = 0
1744 # for subaxis in axis.axislist:
1745 # subaxis.baseaxis = axis
1746 # subaxis._vtickpoint = lambda axis, v: axis.baseaxis._vtickpoint(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1747 # subaxis.vtickdirection = lambda axis, v: axis.baseaxis.vtickdirection(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1748 # subaxis.vbaseline = self.subvbaseline
1749 # subaxis.dolayout(graph)
1750 # if axis.layoutdata._extent < subaxis.layoutdata._extent:
1751 # axis.layoutdata._extent = subaxis.layoutdata._extent
1752 # axistitlepainter.dolayout(self, graph, axis)
1754 # def paint(self, graph, axis):
1755 # for subaxis in axis.axislist:
1756 # subaxis.dopaint(graph)
1757 # if self.breaklinesattrs is not None:
1758 # for subaxis1, subaxis2 in zip(axis.axislist[:-1], axis.axislist[1:]):
1759 # # use a tangent of the baseline (this is independent of the tickdirection)
1760 # v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1761 # breakline = path.normpath(axis.vbaseline(axis, v, None)).tangent(0, self.breaklineslength)
1762 # widthline = path.normpath(axis.vbaseline(axis, v, None)).tangent(0, self.breaklinesdist).transformed(trafomodule.rotate(self.breaklinesangle+90, *breakline.begin()))
1763 # tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1764 # towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1765 # breakline = breakline.transformed(trafomodule.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1766 # breakline1 = breakline.transformed(trafomodule.translate(*towidth))
1767 # breakline2 = breakline.transformed(trafomodule.translate(-towidth[0], -towidth[1]))
1768 # graph.fill(path.path(path.moveto(*breakline1.begin()),
1769 # path.lineto(*breakline1.end()),
1770 # path.lineto(*breakline2.end()),
1771 # path.lineto(*breakline2.begin()),
1772 # path.closepath()), color.gray.white)
1773 # graph.stroke(breakline1, *helper.ensuresequence(self.breaklinesattrs))
1774 # graph.stroke(breakline2, *helper.ensuresequence(self.breaklinesattrs))
1775 # axistitlepainter.paint(self, graph, axis)
1778 class baraxispainter(axistitlepainter): pass
1779 # class baraxispainter(axistitlepainter):
1781 # def __init__(self, innerticklength=None,
1782 # outerticklength=None,
1783 # tickattrs=(),
1784 # baselineattrs=canvas.linecap.square,
1785 # namedist="0.3 cm",
1786 # nameattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1787 # namedirection=None,
1788 # namepos=0.5,
1789 # namehequalize=0,
1790 # namevequalize=1,
1791 # **args):
1792 # self.innerticklength_str = innerticklength
1793 # self.outerticklength_str = outerticklength
1794 # self.tickattrs = tickattrs
1795 # self.baselineattrs = baselineattrs
1796 # self.namedist_str = namedist
1797 # self.nameattrs = nameattrs
1798 # self.namedirection = namedirection
1799 # self.namepos = namepos
1800 # self.namehequalize = namehequalize
1801 # self.namevequalize = namevequalize
1802 # axistitlepainter.__init__(self, **args)
1804 # def dolayout(self, graph, axis):
1805 # axis.layoutdata._extent = 0
1806 # if axis.multisubaxis:
1807 # for name, subaxis in zip(axis.names, axis.subaxis):
1808 # subaxis.vmin = axis.convert((name, 0))
1809 # subaxis.vmax = axis.convert((name, 1))
1810 # subaxis.baseaxis = axis
1811 # subaxis._vtickpoint = lambda axis, v: axis.baseaxis._vtickpoint(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1812 # subaxis.vtickdirection = lambda axis, v: axis.baseaxis.vtickdirection(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1813 # subaxis.vbaseline = None
1814 # subaxis.dolayout(graph)
1815 # if axis.layoutdata._extent < subaxis.layoutdata._extent:
1816 # axis.layoutdata._extent = subaxis.layoutdata._extent
1817 # axis.namepos = []
1818 # for name in axis.names:
1819 # v = axis.convert((name, self.namepos))
1820 # x, y = axis._vtickpoint(axis, v)
1821 # dx, dy = axis.vtickdirection(axis, v)
1822 # axis.namepos.append((v, x, y, dx, dy))
1823 # axis.nameboxes = []
1824 # if self.nameattrs is not None:
1825 # for (v, x, y, dx, dy), name in zip(axis.namepos, axis.names):
1826 # nameattrs = helper.ensurelist(self.nameattrs)
1827 # if self.namedirection is not None:
1828 # nameattrs += [trafomodule.rotate(self.reldirection(self.namedirection, dx, dy))]
1829 # if axis.texts.has_key(name):
1830 # axis.nameboxes.append(graph.texrunner._text(x, y, str(axis.texts[name]), *nameattrs))
1831 # elif axis.texts.has_key(str(name)):
1832 # axis.nameboxes.append(graph.texrunner._text(x, y, str(axis.texts[str(name)]), *nameattrs))
1833 # else:
1834 # axis.nameboxes.append(graph.texrunner._text(x, y, str(name), *nameattrs))
1835 # labeldist = axis.layoutdata._extent + unit.topt(unit.length(self.namedist_str, default_type="v"))
1836 # if len(axis.namepos) > 1:
1837 # equaldirection = 1
1838 # for namepos in axis.namepos[1:]:
1839 # if namepos[3] != axis.namepos[0][3] or namepos[4] != axis.namepos[0][4]:
1840 # equaldirection = 0
1841 # else:
1842 # equaldirection = 0
1843 # if equaldirection and ((not axis.namepos[0][3] and self.namevequalize) or
1844 # (not axis.namepos[0][4] and self.namehequalize)):
1845 # box._linealignequal(axis.nameboxes, labeldist, axis.namepos[0][3], axis.namepos[0][4])
1846 # else:
1847 # for namebox, namepos in zip(axis.nameboxes, axis.namepos):
1848 # namebox._linealign(labeldist, namepos[3], namepos[4])
1849 # if self.innerticklength_str is not None:
1850 # axis.innerticklength = unit.topt(unit.length(self.innerticklength_str, default_type="v"))
1851 # else:
1852 # if self.outerticklength_str is not None:
1853 # axis.innerticklength = 0
1854 # else:
1855 # axis.innerticklength = None
1856 # if self.outerticklength_str is not None:
1857 # axis.outerticklength = unit.topt(unit.length(self.outerticklength_str, default_type="v"))
1858 # else:
1859 # if self.innerticklength_str is not None:
1860 # axis.outerticklength = 0
1861 # else:
1862 # axis.outerticklength = None
1863 # if axis.outerticklength is not None and self.tickattrs is not None:
1864 # axis.layoutdata._extent += axis.outerticklength
1865 # for (v, x, y, dx, dy), namebox in zip(axis.namepos, axis.nameboxes):
1866 # newextent = namebox._extent(dx, dy) + labeldist
1867 # if axis.layoutdata._extent < newextent:
1868 # axis.layoutdata._extent = newextent
1869 # #graph.mindbbox(*[namebox.bbox() for namebox in axis.nameboxes])
1870 # axistitlepainter.dolayout(self, graph, axis)
1872 # def paint(self, graph, axis):
1873 # if axis.subaxis is not None:
1874 # if axis.multisubaxis:
1875 # for subaxis in axis.subaxis:
1876 # subaxis.dopaint(graph)
1877 # if None not in (self.tickattrs, axis.innerticklength, axis.outerticklength):
1878 # for pos in axis.relsizes:
1879 # if pos == axis.relsizes[0]:
1880 # pos -= axis.firstdist
1881 # elif pos != axis.relsizes[-1]:
1882 # pos -= 0.5 * axis.dist
1883 # v = pos / axis.relsizes[-1]
1884 # x, y = axis._vtickpoint(axis, v)
1885 # dx, dy = axis.vtickdirection(axis, v)
1886 # x1 = x - dx * axis.innerticklength
1887 # y1 = y - dy * axis.innerticklength
1888 # x2 = x + dx * axis.outerticklength
1889 # y2 = y + dy * axis.outerticklength
1890 # graph.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(self.tickattrs))
1891 # if self.baselineattrs is not None:
1892 # if axis.vbaseline is not None: # XXX: subbaselines (as for splitlines)
1893 # graph.stroke(axis.vbaseline(axis), *helper.ensuresequence(self.baselineattrs))
1894 # for namebox in axis.nameboxes:
1895 # graph.insert(namebox)
1896 # axistitlepainter.paint(self, graph, axis)
1900 ################################################################################
1901 # axes
1902 ################################################################################
1904 class PartitionError(Exception): pass
1906 class _axis:
1908 def __init__(self, min=None, max=None, reverse=0, divisor=1,
1909 datavmin=None, datavmax=None, tickvmin=0, tickvmax=1,
1910 title=None, painter=axispainter(), texter=defaulttexter(),
1911 dense=None):
1912 if None not in (min, max) and min > max:
1913 min, max, reverse = max, min, not reverse
1914 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
1916 self.datamin = self.datamax = self.tickmin = self.tickmax = None
1917 if datavmin is None:
1918 if self.fixmin:
1919 self.datavmin = 0
1920 else:
1921 self.datavmin = 0.05
1922 else:
1923 self.datavmin = datavmin
1924 if datavmax is None:
1925 if self.fixmax:
1926 self.datavmax = 1
1927 else:
1928 self.datavmax = 0.95
1929 else:
1930 self.datavmax = datavmax
1931 self.tickvmin = tickvmin
1932 self.tickvmax = tickvmax
1934 self.divisor = divisor
1935 self.title = title
1936 self.painter = painter
1937 self.texter = texter
1938 self.dense = dense
1939 self.canconvert = 0
1940 self.__setinternalrange()
1942 def __setinternalrange(self, min=None, max=None):
1943 if not self.fixmin and min is not None and (self.min is None or min < self.min):
1944 self.min = min
1945 if not self.fixmax and max is not None and (self.max is None or max > self.max):
1946 self.max = max
1947 if None not in (self.min, self.max):
1948 min, max, vmin, vmax = self.min, self.max, 0, 1
1949 self.canconvert = 1
1950 self.setbasepoints(((min, vmin), (max, vmax)))
1951 if not self.fixmin:
1952 if self.datamin is not None and self.convert(self.datamin) < self.datavmin:
1953 min, vmin = self.datamin, self.datavmin
1954 self.setbasepoints(((min, vmin), (max, vmax)))
1955 if self.tickmin is not None and self.convert(self.tickmin) < self.tickvmin:
1956 min, vmin = self.tickmin, self.tickvmin
1957 self.setbasepoints(((min, vmin), (max, vmax)))
1958 if not self.fixmax:
1959 if self.datamax is not None and self.convert(self.datamax) > self.datavmax:
1960 max, vmax = self.datamax, self.datavmax
1961 self.setbasepoints(((min, vmin), (max, vmax)))
1962 if self.tickmax is not None and self.convert(self.tickmax) > self.tickvmax:
1963 max, vmax = self.tickmax, self.tickvmax
1964 self.setbasepoints(((min, vmin), (max, vmax)))
1965 if self.reverse:
1966 self.setbasepoints(((min, vmax), (max, vmin)))
1968 def __getinternalrange(self):
1969 return self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax
1971 def __forceinternalrange(self, range):
1972 self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax = range
1973 self.__setinternalrange()
1975 def setdatarange(self, min, max):
1976 self.datamin, self.datamax = min, max
1977 self.__setinternalrange(min, max)
1979 def settickrange(self, min, max):
1980 self.tickmin, self.tickmax = min, max
1981 self.__setinternalrange(min, max)
1983 def getdatarange(self):
1984 if self.canconvert:
1985 if self.reverse:
1986 return self.invert(1-self.datavmin), self.invert(1-self.datavmax)
1987 else:
1988 return self.invert(self.datavmin), self.invert(self.datavmax)
1990 def gettickrange(self):
1991 if self.canconvert:
1992 if self.reverse:
1993 return self.invert(1-self.tickvmin), self.invert(1-self.tickvmax)
1994 else:
1995 return self.invert(self.tickvmin), self.invert(self.tickvmax)
1997 def dolayout(self, graph):
1998 if self.dense is not None:
1999 dense = self.dense
2000 else:
2001 dense = graph.dense
2002 min, max = self.gettickrange()
2003 if self.part is not None:
2004 self.ticks = self.part.defaultpart(min/self.divisor,
2005 max/self.divisor,
2006 not self.fixmin,
2007 not self.fixmax)
2008 else:
2009 self.ticks = []
2010 # lesspart and morepart can be called after defaultpart,
2011 # although some axes may share their autoparting ---
2012 # it works, because the axes are processed sequentially
2013 first = 1
2014 maxworse = 2
2015 worse = 0
2016 while worse < maxworse:
2017 if self.part is not None:
2018 newticks = self.part.lesspart()
2019 else:
2020 newticks = None
2021 if newticks is not None:
2022 if first:
2023 bestrate = self.rate.ratepart(self, self.ticks, dense)
2024 variants = [[bestrate, self.ticks]]
2025 first = 0
2026 newrate = self.rate.ratepart(self, newticks, dense)
2027 variants.append([newrate, newticks])
2028 if newrate < bestrate:
2029 bestrate = newrate
2030 worse = 0
2031 else:
2032 worse += 1
2033 else:
2034 worse += 1
2035 worse = 0
2036 while worse < maxworse:
2037 if self.part is not None:
2038 newticks = self.part.morepart()
2039 else:
2040 newticks = None
2041 if newticks is not None:
2042 if first:
2043 bestrate = self.rate.ratepart(self, self.ticks, dense)
2044 variants = [[bestrate, self.ticks]]
2045 first = 0
2046 newrate = self.rate.ratepart(self, newticks, dense)
2047 variants.append([newrate, newticks])
2048 if newrate < bestrate:
2049 bestrate = newrate
2050 worse = 0
2051 else:
2052 worse += 1
2053 else:
2054 worse += 1
2055 if not first:
2056 variants.sort()
2057 if self.painter is not None:
2058 i = 0
2059 bestrate = None
2060 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
2061 saverange = self.__getinternalrange()
2062 self.ticks = variants[i][1]
2063 if len(self.ticks):
2064 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2065 layoutdata = axiscanvas()
2066 self.texter.labels(self.ticks)
2067 self.painter.paint(graph, self, self.ticks, layoutdata)
2068 ratelayout = self.rate.ratelayout(layoutdata, dense)
2069 if ratelayout is not None:
2070 variants[i][0] += ratelayout
2071 variants[i].append(layoutdata)
2072 else:
2073 variants[i][0] = None
2074 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
2075 bestrate = variants[i][0]
2076 self.__forceinternalrange(saverange)
2077 i += 1
2078 if bestrate is None:
2079 raise PartitionError("no valid axis partitioning found")
2080 variants = [variant for variant in variants[:i] if variant[0] is not None]
2081 variants.sort()
2082 self.ticks = variants[0][1]
2083 self.layoutdata = variants[0][2]
2084 else:
2085 for tick in self.ticks:
2086 tick.textbox = None
2087 self.layoutdata = axiscanvas()
2088 if len(self.ticks):
2089 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2090 else:
2091 if len(self.ticks):
2092 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2093 self.layoutdata = canvas.canvas()
2094 self.texter.labels(self.ticks)
2095 self.painter.paint(graph, self, self.ticks, self.layoutdata)
2096 #self.layoutdata = layoutdata
2097 #graph.mindbbox(*[tick.textbox.bbox() for tick in self.ticks if tick.textbox is not None])
2099 def dopaint(self, graph):
2100 graph.insert(self.layoutdata)
2102 def createlinkaxis(self, **args):
2103 return linkaxis(self, **args)
2106 class linaxis(_axis, _linmap):
2108 def __init__(self, part=autolinpart(), rate=axisrater(), **args):
2109 _axis.__init__(self, **args)
2110 if self.fixmin and self.fixmax:
2111 self.relsize = self.max - self.min
2112 self.part = part
2113 self.rate = rate
2116 class logaxis(_axis, _logmap):
2118 def __init__(self, part=autologpart(), rate=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
2119 _axis.__init__(self, **args)
2120 if self.fixmin and self.fixmax:
2121 self.relsize = math.log(self.max) - math.log(self.min)
2122 self.part = part
2123 self.rate = rate
2126 class linkaxis:
2128 def __init__(self, linkedaxis, title=None, skipticklevel=None, skiplabellevel=0, painter=linkaxispainter()):
2129 self.linkedaxis = linkedaxis
2130 while isinstance(self.linkedaxis, linkaxis):
2131 self.linkedaxis = self.linkedaxis.linkedaxis
2132 self.fixmin = self.linkedaxis.fixmin
2133 self.fixmax = self.linkedaxis.fixmax
2134 if self.fixmin:
2135 self.min = self.linkedaxis.min
2136 if self.fixmax:
2137 self.max = self.linkedaxis.max
2138 self.skipticklevel = skipticklevel
2139 self.skiplabellevel = skiplabellevel
2140 self.title = title
2141 self.painter = painter
2143 def getdatarange(self):
2144 return self.linkedaxis.getdatarange()
2146 def setdatarange(self, min, max):
2147 prevrange = self.linkedaxis.getdatarange()
2148 self.linkedaxis.setdatarange(min, max)
2149 if hasattr(self.linkedaxis, "ticks") and prevrange != self.linkedaxis.getdatarange():
2150 raise RuntimeError("linkaxis datarange setting performed while linked axis layout already fixed")
2152 def dolayout(self, graph):
2153 self.convert = self.linkedaxis.convert
2154 self.divisor = self.linkedaxis.divisor
2155 self.layoutdata = axiscanvas()
2156 self.painter.paint(graph, self, self.linkedaxis.ticks, self.layoutdata)
2158 def dopaint(self, graph):
2159 graph.insert(self.layoutdata)
2161 def createlinkaxis(self, **args):
2162 return linkaxis(self.linkedaxis)
2165 class splitaxis:
2167 def __init__(self, axislist, splitlist=0.5, splitdist=0.1, relsizesplitdist=1, title=None, painter=splitaxispainter()):
2168 self.title = title
2169 self.axislist = axislist
2170 self.painter = painter
2171 self.splitlist = list(helper.ensuresequence(splitlist))
2172 self.splitlist.sort()
2173 if len(self.axislist) != len(self.splitlist) + 1:
2174 for subaxis in self.axislist:
2175 if not isinstance(subaxis, linkaxis):
2176 raise ValueError("axislist and splitlist lengths do not fit together")
2177 for subaxis in self.axislist:
2178 if isinstance(subaxis, linkaxis):
2179 subaxis.vmin = subaxis.linkedaxis.vmin
2180 subaxis.vminover = subaxis.linkedaxis.vminover
2181 subaxis.vmax = subaxis.linkedaxis.vmax
2182 subaxis.vmaxover = subaxis.linkedaxis.vmaxover
2183 else:
2184 subaxis.vmin = None
2185 subaxis.vmax = None
2186 self.axislist[0].vmin = 0
2187 self.axislist[0].vminover = None
2188 self.axislist[-1].vmax = 1
2189 self.axislist[-1].vmaxover = None
2190 for i in xrange(len(self.splitlist)):
2191 if self.splitlist[i] is not None:
2192 self.axislist[i].vmax = self.splitlist[i] - 0.5*splitdist
2193 self.axislist[i].vmaxover = self.splitlist[i]
2194 self.axislist[i+1].vmin = self.splitlist[i] + 0.5*splitdist
2195 self.axislist[i+1].vminover = self.splitlist[i]
2196 i = 0
2197 while i < len(self.axislist):
2198 if self.axislist[i].vmax is None:
2199 j = relsize = relsize2 = 0
2200 while self.axislist[i + j].vmax is None:
2201 relsize += self.axislist[i + j].relsize + relsizesplitdist
2202 j += 1
2203 relsize += self.axislist[i + j].relsize
2204 vleft = self.axislist[i].vmin
2205 vright = self.axislist[i + j].vmax
2206 for k in range(i, i + j):
2207 relsize2 += self.axislist[k].relsize
2208 self.axislist[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
2209 relsize2 += 0.5 * relsizesplitdist
2210 self.axislist[k].vmaxover = self.axislist[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
2211 relsize2 += 0.5 * relsizesplitdist
2212 self.axislist[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
2213 if i == 0 and i + j + 1 == len(self.axislist):
2214 self.relsize = relsize
2215 i += j + 1
2216 else:
2217 i += 1
2219 self.fixmin = self.axislist[0].fixmin
2220 if self.fixmin:
2221 self.min = self.axislist[0].min
2222 self.fixmax = self.axislist[-1].fixmax
2223 if self.fixmax:
2224 self.max = self.axislist[-1].max
2225 self.divisor = 1
2227 def getdatarange(self):
2228 min = self.axislist[0].getdatarange()
2229 max = self.axislist[-1].getdatarange()
2230 try:
2231 return min[0], max[1]
2232 except TypeError:
2233 return None
2235 def setdatarange(self, min, max):
2236 self.axislist[0].setdatarange(min, None)
2237 self.axislist[-1].setdatarange(None, max)
2239 def gettickrange(self):
2240 min = self.axislist[0].gettickrange()
2241 max = self.axislist[-1].gettickrange()
2242 try:
2243 return min[0], max[1]
2244 except TypeError:
2245 return None
2247 def settickrange(self, min, max):
2248 self.axislist[0].settickrange(min, None)
2249 self.axislist[-1].settickrange(None, max)
2251 def convert(self, value):
2252 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2253 if value < self.axislist[0].max:
2254 return self.axislist[0].vmin + self.axislist[0].convert(value)*(self.axislist[0].vmax-self.axislist[0].vmin)
2255 for axis in self.axislist[1:-1]:
2256 if value > axis.min and value < axis.max:
2257 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
2258 if value > self.axislist[-1].min:
2259 return self.axislist[-1].vmin + self.axislist[-1].convert(value)*(self.axislist[-1].vmax-self.axislist[-1].vmin)
2260 raise ValueError("value couldn't be assigned to a split region")
2262 def dolayout(self, graph):
2263 self.layoutdata = layoutdata()
2264 self.painter.dolayout(graph, self)
2266 def dopaint(self, graph):
2267 self.painter.paint(graph, self)
2269 def createlinkaxis(self, painter=None, *args):
2270 if not len(args):
2271 return splitaxis([x.createlinkaxis() for x in self.axislist], splitlist=None)
2272 if len(args) != len(self.axislist):
2273 raise IndexError("length of the argument list doesn't fit to split number")
2274 if painter is None:
2275 painter = self.painter
2276 return splitaxis([x.createlinkaxis(**arg) for x, arg in zip(self.axislist, args)], painter=painter)
2279 class baraxis:
2281 def __init__(self, subaxis=None, multisubaxis=0, title=None, dist=0.5, firstdist=None, lastdist=None, names=None, texts={}, painter=baraxispainter()):
2282 self.dist = dist
2283 if firstdist is not None:
2284 self.firstdist = firstdist
2285 else:
2286 self.firstdist = 0.5 * dist
2287 if lastdist is not None:
2288 self.lastdist = lastdist
2289 else:
2290 self.lastdist = 0.5 * dist
2291 self.relsizes = None
2292 self.fixnames = 0
2293 self.names = []
2294 for name in helper.ensuresequence(names):
2295 self.setname(name)
2296 self.fixnames = names is not None
2297 self.multisubaxis = multisubaxis
2298 if self.multisubaxis:
2299 self.createsubaxis = subaxis
2300 self.subaxis = [self.createsubaxis.createsubaxis() for name in self.names]
2301 else:
2302 self.subaxis = subaxis
2303 self.title = title
2304 self.fixnames = 0
2305 self.texts = texts
2306 self.painter = painter
2308 def getdatarange(self):
2309 return None
2311 def setname(self, name, *subnames):
2312 # setting self.relsizes to None forces later recalculation
2313 if not self.fixnames:
2314 if name not in self.names:
2315 self.relsizes = None
2316 self.names.append(name)
2317 if self.multisubaxis:
2318 self.subaxis.append(self.createsubaxis.createsubaxis())
2319 if (not self.fixnames or name in self.names) and len(subnames):
2320 if self.multisubaxis:
2321 if self.subaxis[self.names.index(name)].setname(*subnames):
2322 self.relsizes = None
2323 else:
2324 #print self.subaxis, self.multisubaxis, subnames, name
2325 if self.subaxis.setname(*subnames):
2326 self.relsizes = None
2327 return self.relsizes is not None
2329 def updaterelsizes(self):
2330 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
2331 self.relsizes[-1] += self.lastdist - self.dist
2332 if self.multisubaxis:
2333 subrelsize = 0
2334 for i in range(1, len(self.relsizes)):
2335 self.subaxis[i-1].updaterelsizes()
2336 subrelsize += self.subaxis[i-1].relsizes[-1]
2337 self.relsizes[i] += subrelsize
2338 else:
2339 if self.subaxis is None:
2340 subrelsize = 1
2341 else:
2342 self.subaxis.updaterelsizes()
2343 subrelsize = self.subaxis.relsizes[-1]
2344 for i in range(1, len(self.relsizes)):
2345 self.relsizes[i] += i * subrelsize
2347 def convert(self, value):
2348 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2349 if not self.relsizes:
2350 self.updaterelsizes()
2351 pos = self.names.index(value[0])
2352 if len(value) == 2:
2353 if self.subaxis is None:
2354 subvalue = value[1]
2355 else:
2356 if self.multisubaxis:
2357 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
2358 else:
2359 subvalue = value[1] * self.subaxis.relsizes[-1]
2360 else:
2361 if self.multisubaxis:
2362 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
2363 else:
2364 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
2365 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
2367 def dolayout(self, graph):
2368 self.layoutdata = layoutdata()
2369 self.painter.dolayout(graph, self)
2371 def dopaint(self, graph):
2372 self.painter.paint(graph, self)
2374 def createlinkaxis(self, **args):
2375 if self.subaxis is not None:
2376 if self.multisubaxis:
2377 subaxis = [subaxis.createlinkaxis() for subaxis in self.subaxis]
2378 else:
2379 subaxis = self.subaxis.createlinkaxis()
2380 else:
2381 subaxis = None
2382 return baraxis(subaxis=subaxis, dist=self.dist, firstdist=self.firstdist, lastdist=self.lastdist, **args)
2384 createsubaxis = createlinkaxis
2387 ################################################################################
2388 # graph key
2389 ################################################################################
2392 # g = graph.graphxy(key=graph.key())
2393 # g.addkey(graph.key(), ...)
2396 class key:
2398 def __init__(self, dist="0.2 cm", pos = "tr", hinside = 1, vinside = 1, hdist="0.6 cm", vdist="0.4 cm",
2399 symbolwidth="0.5 cm", symbolheight="0.25 cm", symbolspace="0.2 cm",
2400 textattrs=textmodule.vshift.mathaxis):
2401 self.dist_str = dist
2402 self.pos = pos
2403 self.hinside = hinside
2404 self.vinside = vinside
2405 self.hdist_str = hdist
2406 self.vdist_str = vdist
2407 self.symbolwidth_str = symbolwidth
2408 self.symbolheight_str = symbolheight
2409 self.symbolspace_str = symbolspace
2410 self.textattrs = textattrs
2411 self.plotinfos = None
2412 if self.pos in ("tr", "rt"):
2413 self.right = 1
2414 self.top = 1
2415 elif self.pos in ("br", "rb"):
2416 self.right = 1
2417 self.top = 0
2418 elif self.pos in ("tl", "lt"):
2419 self.right = 0
2420 self.top = 1
2421 elif self.pos in ("bl", "lb"):
2422 self.right = 0
2423 self.top = 0
2424 else:
2425 raise RuntimeError("invalid pos attribute")
2427 def setplotinfos(self, *plotinfos):
2428 """set the plotinfos to be used in the key
2429 - call it exactly once"""
2430 if self.plotinfos is not None:
2431 raise RuntimeError("setplotinfo is called multiple times")
2432 self.plotinfos = plotinfos
2434 def dolayout(self, graph):
2435 "creates the layout of the key"
2436 self._dist = unit.topt(unit.length(self.dist_str, default_type="v"))
2437 self._hdist = unit.topt(unit.length(self.hdist_str, default_type="v"))
2438 self._vdist = unit.topt(unit.length(self.vdist_str, default_type="v"))
2439 self._symbolwidth = unit.topt(unit.length(self.symbolwidth_str, default_type="v"))
2440 self._symbolheight = unit.topt(unit.length(self.symbolheight_str, default_type="v"))
2441 self._symbolspace = unit.topt(unit.length(self.symbolspace_str, default_type="v"))
2442 self.titles = []
2443 for plotinfo in self.plotinfos:
2444 self.titles.append(graph.texrunner._text(0, 0, plotinfo.data.title, *helper.ensuresequence(self.textattrs)))
2445 box._tile(self.titles, self._dist, 0, -1)
2446 box._linealignequal(self.titles, self._symbolwidth + self._symbolspace, 1, 0)
2448 def bbox(self):
2449 """return a bbox for the key
2450 method should be called after dolayout"""
2451 result = self.titles[0].bbox()
2452 for title in self.titles[1:]:
2453 result = result + title.bbox() + bbox._bbox(0, title.center[1] - 0.5 * self._symbolheight,
2454 0, title.center[1] + 0.5 * self._symbolheight)
2455 return result
2457 def paint(self, c, x, y):
2458 """paint the graph key into a canvas c at the position x and y (in postscript points)
2459 - method should be called after dolayout
2460 - the x, y alignment might be calculated by the graph using:
2461 - the bbox of the key as returned by the keys bbox method
2462 - the attributes _hdist, _vdist, hinside, and vinside of the key
2463 - the dimension and geometry of the graph"""
2464 sc = c.insert(canvas.canvas(trafomodule._translate(x, y)))
2465 for plotinfo, title in zip(self.plotinfos, self.titles):
2466 plotinfo.style.key(sc, 0, -0.5 * self._symbolheight + title.center[1],
2467 self._symbolwidth, self._symbolheight)
2468 sc.insert(title)
2471 ################################################################################
2472 # graph
2473 ################################################################################
2476 class plotinfo:
2478 def __init__(self, data, style):
2479 self.data = data
2480 self.style = style
2483 class graphxy(canvas.canvas):
2485 Names = "x", "y"
2487 def clipcanvas(self):
2488 return self.insert(canvas.canvas(canvas.clip(path._rect(self._xpos, self._ypos, self._width, self._height))))
2490 def plot(self, data, style=None):
2491 if self.haslayout:
2492 raise RuntimeError("layout setup was already performed")
2493 if style is None:
2494 if helper.issequence(data):
2495 raise RuntimeError("list plot needs an explicit style")
2496 if self.defaultstyle.has_key(data.defaultstyle):
2497 style = self.defaultstyle[data.defaultstyle].iterate()
2498 else:
2499 style = data.defaultstyle()
2500 self.defaultstyle[data.defaultstyle] = style
2501 plotinfos = []
2502 first = 1
2503 for d in helper.ensuresequence(data):
2504 if not first:
2505 style = style.iterate()
2506 first = 0
2507 if d is not None:
2508 d.setstyle(self, style)
2509 plotinfos.append(plotinfo(d, style))
2510 self.plotinfos.extend(plotinfos)
2511 if helper.issequence(data):
2512 return plotinfos
2513 return plotinfos[0]
2515 def addkey(self, key, *plotinfos):
2516 if self.haslayout:
2517 raise RuntimeError("layout setup was already performed")
2518 self.addkeys.append((key, plotinfos))
2520 def _vxtickpoint(self, axis, v):
2521 return (self._xpos+v*self._width, axis.axispos)
2523 def _vytickpoint(self, axis, v):
2524 return (axis.axispos, self._ypos+v*self._height)
2526 def vtickdirection(self, axis, v):
2527 return axis.fixtickdirection
2529 def _pos(self, x, y, xaxis=None, yaxis=None):
2530 if xaxis is None: xaxis = self.axes["x"]
2531 if yaxis is None: yaxis = self.axes["y"]
2532 return self._xpos+xaxis.convert(x)*self._width, self._ypos+yaxis.convert(y)*self._height
2534 def pos(self, x, y, xaxis=None, yaxis=None):
2535 if xaxis is None: xaxis = self.axes["x"]
2536 if yaxis is None: yaxis = self.axes["y"]
2537 return self.xpos+xaxis.convert(x)*self.width, self.ypos+yaxis.convert(y)*self.height
2539 def _vpos(self, vx, vy):
2540 return self._xpos+vx*self._width, self._ypos+vy*self._height
2542 def vpos(self, vx, vy):
2543 return self.xpos+vx*self.width, self.ypos+vy*self.height
2545 def xbaseline(self, axis, x1, x2, shift=0, xaxis=None):
2546 if xaxis is None: xaxis = self.axes["x"]
2547 v1, v2 = xaxis.convert(x1), xaxis.convert(x2)
2548 return path._line(self._xpos+v1*self._width, axis.axispos+shift,
2549 self._xpos+v2*self._width, axis.axispos+shift)
2551 def ybaseline(self, axis, y1, y2, shift=0, yaxis=None):
2552 if yaxis is None: yaxis = self.axes["y"]
2553 v1, v2 = yaxis.convert(y1), yaxis.convert(y2)
2554 return path._line(axis.axispos+shift, self._ypos+v1*self._height,
2555 axis.axispos+shift, self._ypos+v2*self._height)
2557 def vxbaseline(self, axis, v1=None, v2=None, shift=0):
2558 if v1 is None: v1 = 0
2559 if v2 is None: v2 = 1
2560 return path._line(self._xpos+v1*self._width, axis.axispos+shift,
2561 self._xpos+v2*self._width, axis.axispos+shift)
2563 def vybaseline(self, axis, v1=None, v2=None, shift=0):
2564 if v1 is None: v1 = 0
2565 if v2 is None: v2 = 1
2566 return path._line(axis.axispos+shift, self._ypos+v1*self._height,
2567 axis.axispos+shift, self._ypos+v2*self._height)
2569 def xgridpath(self, x, xaxis=None):
2570 if xaxis is None: xaxis = self.axes["x"]
2571 v = xaxis.convert(x)
2572 return path._line(self._xpos+v*self._width, self._ypos,
2573 self._xpos+v*self._width, self._ypos+self._height)
2575 def ygridpath(self, y, yaxis=None):
2576 if yaxis is None: yaxis = self.axes["y"]
2577 v = yaxis.convert(y)
2578 return path._line(self._xpos, self._ypos+v*self._height,
2579 self._xpos+self._width, self._ypos+v*self._height)
2581 def vxgridpath(self, v):
2582 return path._line(self._xpos+v*self._width, self._ypos,
2583 self._xpos+v*self._width, self._ypos+self._height)
2585 def vygridpath(self, v):
2586 return path._line(self._xpos, self._ypos+v*self._height,
2587 self._xpos+self._width, self._ypos+v*self._height)
2589 def _addpos(self, x, y, dx, dy):
2590 return x+dx, y+dy
2592 def _connect(self, x1, y1, x2, y2):
2593 return path._lineto(x2, y2)
2595 def keynum(self, key):
2596 try:
2597 while key[0] in string.letters:
2598 key = key[1:]
2599 return int(key)
2600 except IndexError:
2601 return 1
2603 def gatherranges(self):
2604 ranges = {}
2605 for plotinfo in self.plotinfos:
2606 pdranges = plotinfo.data.getranges()
2607 if pdranges is not None:
2608 for key in pdranges.keys():
2609 if key not in ranges.keys():
2610 ranges[key] = pdranges[key]
2611 else:
2612 ranges[key] = (min(ranges[key][0], pdranges[key][0]),
2613 max(ranges[key][1], pdranges[key][1]))
2614 # known ranges are also set as ranges for the axes
2615 for key, axis in self.axes.items():
2616 if key in ranges.keys():
2617 axis.setdatarange(*ranges[key])
2618 ranges[key] = axis.getdatarange()
2619 if ranges[key] is None:
2620 del ranges[key]
2621 return ranges
2623 def removedomethod(self, method):
2624 hadmethod = 0
2625 while 1:
2626 try:
2627 self.domethods.remove(method)
2628 hadmethod = 1
2629 except ValueError:
2630 return hadmethod
2632 def dolayout(self):
2633 if not self.removedomethod(self.dolayout): return
2634 self.haslayout = 1
2635 # create list of ranges
2636 # 1. gather ranges
2637 ranges = self.gatherranges()
2638 # 2. calculate additional ranges out of known ranges
2639 for plotinfo in self.plotinfos:
2640 plotinfo.data.setranges(ranges)
2641 # 3. gather ranges again
2642 self.gatherranges()
2644 # do the layout for all axes
2645 axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
2646 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
2647 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
2648 self._xaxisextents = [0, 0]
2649 self._yaxisextents = [0, 0]
2650 needxaxisdist = [0, 0]
2651 needyaxisdist = [0, 0]
2652 items = list(self.axes.items())
2653 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
2654 for key, axis in items:
2655 num = self.keynum(key)
2656 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
2657 num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
2658 if XPattern.match(key):
2659 if needxaxisdist[num2]:
2660 self._xaxisextents[num2] += axesdist
2661 axis.axispos = self._ypos+num2*self._height + num3*self._xaxisextents[num2]
2662 axis._vtickpoint = self._vxtickpoint
2663 axis.fixtickdirection = (0, num3)
2664 axis.vgridpath = self.vxgridpath
2665 axis.vbaseline = self.vxbaseline
2666 axis.gridpath = self.xgridpath
2667 axis.baseline = self.xbaseline
2668 elif YPattern.match(key):
2669 if needyaxisdist[num2]:
2670 self._yaxisextents[num2] += axesdist
2671 axis.axispos = self._xpos+num2*self._width + num3*self._yaxisextents[num2]
2672 axis._vtickpoint = self._vytickpoint
2673 axis.fixtickdirection = (num3, 0)
2674 axis.vgridpath = self.vygridpath
2675 axis.vbaseline = self.vybaseline
2676 axis.gridpath = self.ygridpath
2677 axis.baseline = self.ybaseline
2678 else:
2679 raise ValueError("Axis key '%s' not allowed" % key)
2680 axis.vtickdirection = self.vtickdirection
2681 axis.dolayout(self)
2682 # TODO: this is totally broken !!!
2683 if XPattern.match(key):
2684 try: self._xaxisextents[num2] += axis.layoutdata._extent
2685 except: pass
2686 if YPattern.match(key):
2687 try: self._yaxisextents[num2] += axis.layoutdata._extent
2688 except: pass
2690 def dobackground(self):
2691 self.dolayout()
2692 if not self.removedomethod(self.dobackground): return
2693 if self.backgroundattrs is not None:
2694 self.draw(path._rect(self._xpos, self._ypos, self._width, self._height),
2695 *helper.ensuresequence(self.backgroundattrs))
2697 def doaxes(self):
2698 self.dolayout()
2699 if not self.removedomethod(self.doaxes): return
2700 for axis in self.axes.values():
2701 axis.dopaint(self)
2703 def dodata(self):
2704 self.dolayout()
2705 if not self.removedomethod(self.dodata): return
2706 for plotinfo in self.plotinfos:
2707 plotinfo.data.draw(self)
2709 def _dokey(self, key, *plotinfos):
2710 key.setplotinfos(*plotinfos)
2711 key.dolayout(self)
2712 bbox = key.bbox()
2713 if key.right:
2714 if key.hinside:
2715 x = self._xpos + self._width - bbox.urx - key._hdist
2716 else:
2717 x = self._xpos + self._width - bbox.llx + key._hdist
2718 else:
2719 if key.hinside:
2720 x = self._xpos - bbox.llx + key._hdist
2721 else:
2722 x = self._xpos - bbox.urx - key._hdist
2723 if key.top:
2724 if key.vinside:
2725 y = self._ypos + self._height - bbox.ury - key._vdist
2726 else:
2727 y = self._ypos + self._height - bbox.lly + key._vdist
2728 else:
2729 if key.vinside:
2730 y = self._ypos - bbox.lly + key._vdist
2731 else:
2732 y = self._ypos - bbox.ury - key._vdist
2733 #self.mindbbox(bbox.transformed(trafomodule._translate(x, y)))
2734 key.paint(self, x, y)
2736 def dokey(self):
2737 self.dolayout()
2738 if not self.removedomethod(self.dokey): return
2739 if self.key is not None:
2740 self._dokey(self.key, *self.plotinfos)
2741 for key, plotinfos in self.addkeys:
2742 self._dokey(key, *plotinfos)
2744 def finish(self):
2745 while len(self.domethods):
2746 self.domethods[0]()
2748 def initwidthheight(self, width, height, ratio):
2749 if (width is not None) and (height is None):
2750 self.width = unit.length(width)
2751 self.height = (1.0/ratio) * self.width
2752 elif (height is not None) and (width is None):
2753 self.height = unit.length(height)
2754 self.width = ratio * self.height
2755 else:
2756 self.width = unit.length(width)
2757 self.height = unit.length(height)
2758 self._width = unit.topt(self.width)
2759 self._height = unit.topt(self.height)
2760 if self._width <= 0: raise ValueError("width <= 0")
2761 if self._height <= 0: raise ValueError("height <= 0")
2763 def initaxes(self, axes, addlinkaxes=0):
2764 for key in self.Names:
2765 if not axes.has_key(key):
2766 axes[key] = linaxis()
2767 elif axes[key] is None:
2768 del axes[key]
2769 if addlinkaxes:
2770 if not axes.has_key(key + "2") and axes.has_key(key):
2771 axes[key + "2"] = axes[key].createlinkaxis()
2772 elif axes[key + "2"] is None:
2773 del axes[key + "2"]
2774 self.axes = axes
2776 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
2777 key=None, backgroundattrs=None, dense=1, axesdist="0.8 cm", **axes):
2778 canvas.canvas.__init__(self)
2779 self.xpos = unit.length(xpos)
2780 self.ypos = unit.length(ypos)
2781 self._xpos = unit.topt(self.xpos)
2782 self._ypos = unit.topt(self.ypos)
2783 self.initwidthheight(width, height, ratio)
2784 self.initaxes(axes, 1)
2785 self.key = key
2786 self.backgroundattrs = backgroundattrs
2787 self.dense = dense
2788 self.axesdist_str = axesdist
2789 self.plotinfos = []
2790 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata, self.dokey]
2791 self.haslayout = 0
2792 self.defaultstyle = {}
2793 self.addkeys = []
2794 #self.mindbboxes = []
2796 #def mindbbox(self, *boxes):
2797 #self.mindbboxes.extend(boxes)
2799 def bbox(self):
2800 self.finish()
2801 result = bbox._bbox(self._xpos - self._yaxisextents[0],
2802 self._ypos - self._xaxisextents[0],
2803 self._xpos + self._width + self._yaxisextents[1],
2804 self._ypos + self._height + self._xaxisextents[1])
2805 #for box in self.mindbboxes:
2806 #result = result + box
2807 return result
2809 def write(self, file):
2810 self.finish()
2811 canvas.canvas.write(self, file)
2815 # some thoughts, but deferred right now
2817 # class graphxyz(graphxy):
2819 # Names = "x", "y", "z"
2821 # def _vxtickpoint(self, axis, v):
2822 # return self._vpos(v, axis.vypos, axis.vzpos)
2824 # def _vytickpoint(self, axis, v):
2825 # return self._vpos(axis.vxpos, v, axis.vzpos)
2827 # def _vztickpoint(self, axis, v):
2828 # return self._vpos(axis.vxpos, axis.vypos, v)
2830 # def vxtickdirection(self, axis, v):
2831 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
2832 # x2, y2 = self._vpos(v, 0.5, 0)
2833 # dx, dy = x1 - x2, y1 - y2
2834 # norm = math.sqrt(dx*dx + dy*dy)
2835 # return dx/norm, dy/norm
2837 # def vytickdirection(self, axis, v):
2838 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
2839 # x2, y2 = self._vpos(0.5, v, 0)
2840 # dx, dy = x1 - x2, y1 - y2
2841 # norm = math.sqrt(dx*dx + dy*dy)
2842 # return dx/norm, dy/norm
2844 # def vztickdirection(self, axis, v):
2845 # return -1, 0
2846 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
2847 # x2, y2 = self._vpos(0.5, 0.5, v)
2848 # dx, dy = x1 - x2, y1 - y2
2849 # norm = math.sqrt(dx*dx + dy*dy)
2850 # return dx/norm, dy/norm
2852 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
2853 # if xaxis is None: xaxis = self.axes["x"]
2854 # if yaxis is None: yaxis = self.axes["y"]
2855 # if zaxis is None: zaxis = self.axes["z"]
2856 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
2858 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
2859 # if xaxis is None: xaxis = self.axes["x"]
2860 # if yaxis is None: yaxis = self.axes["y"]
2861 # if zaxis is None: zaxis = self.axes["z"]
2862 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
2864 # def _vpos(self, vx, vy, vz):
2865 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
2866 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
2867 # + self.a[2]*self.b[0]*(y-self.eye[1])
2868 # + self.a[1]*self.b[2]*(x-self.eye[0])
2869 # - self.a[2]*self.b[1]*(x-self.eye[0])
2870 # - self.a[0]*self.b[2]*(y-self.eye[1])
2871 # - self.a[1]*self.b[0]*(z-self.eye[2]))
2872 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
2873 # + self.eye[2]*self.b[0]*(y-self.eye[1])
2874 # + self.eye[1]*self.b[2]*(x-self.eye[0])
2875 # - self.eye[2]*self.b[1]*(x-self.eye[0])
2876 # - self.eye[0]*self.b[2]*(y-self.eye[1])
2877 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
2878 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
2879 # + self.a[2]*self.eye[0]*(y-self.eye[1])
2880 # + self.a[1]*self.eye[2]*(x-self.eye[0])
2881 # - self.a[2]*self.eye[1]*(x-self.eye[0])
2882 # - self.a[0]*self.eye[2]*(y-self.eye[1])
2883 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
2884 # return da/d0 + self._xpos, db/d0 + self._ypos
2886 # def vpos(self, vx, vy, vz):
2887 # tx, ty = self._vpos(vx, vy, vz)
2888 # return unit.t_pt(tx), unit.t_pt(ty)
2890 # def xbaseline(self, axis, x1, x2, shift=0, xaxis=None):
2891 # if xaxis is None: xaxis = self.axes["x"]
2892 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2), shift)
2894 # def ybaseline(self, axis, y1, y2, shift=0, yaxis=None):
2895 # if yaxis is None: yaxis = self.axes["y"]
2896 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2), shift)
2898 # def zbaseline(self, axis, z1, z2, shift=0, zaxis=None):
2899 # if zaxis is None: zaxis = self.axes["z"]
2900 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2), shift)
2902 # def vxbaseline(self, axis, v1, v2, shift=0):
2903 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
2904 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
2905 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
2906 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
2908 # def vybaseline(self, axis, v1, v2, shift=0):
2909 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
2910 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
2911 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
2912 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
2914 # def vzbaseline(self, axis, v1, v2, shift=0):
2915 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
2916 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
2917 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
2918 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
2920 # def xgridpath(self, x, xaxis=None):
2921 # assert 0
2922 # if xaxis is None: xaxis = self.axes["x"]
2923 # v = xaxis.convert(x)
2924 # return path._line(self._xpos+v*self._width, self._ypos,
2925 # self._xpos+v*self._width, self._ypos+self._height)
2927 # def ygridpath(self, y, yaxis=None):
2928 # assert 0
2929 # if yaxis is None: yaxis = self.axes["y"]
2930 # v = yaxis.convert(y)
2931 # return path._line(self._xpos, self._ypos+v*self._height,
2932 # self._xpos+self._width, self._ypos+v*self._height)
2934 # def zgridpath(self, z, zaxis=None):
2935 # assert 0
2936 # if zaxis is None: zaxis = self.axes["z"]
2937 # v = zaxis.convert(z)
2938 # return path._line(self._xpos, self._zpos+v*self._height,
2939 # self._xpos+self._width, self._zpos+v*self._height)
2941 # def vxgridpath(self, v):
2942 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
2943 # path._lineto(*self._vpos(v, 0, 1)),
2944 # path._lineto(*self._vpos(v, 1, 1)),
2945 # path._lineto(*self._vpos(v, 1, 0)),
2946 # path.closepath())
2948 # def vygridpath(self, v):
2949 # return path.path(path._moveto(*self._vpos(0, v, 0)),
2950 # path._lineto(*self._vpos(0, v, 1)),
2951 # path._lineto(*self._vpos(1, v, 1)),
2952 # path._lineto(*self._vpos(1, v, 0)),
2953 # path.closepath())
2955 # def vzgridpath(self, v):
2956 # return path.path(path._moveto(*self._vpos(0, 0, v)),
2957 # path._lineto(*self._vpos(0, 1, v)),
2958 # path._lineto(*self._vpos(1, 1, v)),
2959 # path._lineto(*self._vpos(1, 0, v)),
2960 # path.closepath())
2962 # def _addpos(self, x, y, dx, dy):
2963 # assert 0
2964 # return x+dx, y+dy
2966 # def _connect(self, x1, y1, x2, y2):
2967 # assert 0
2968 # return path._lineto(x2, y2)
2970 # def doaxes(self):
2971 # self.dolayout()
2972 # if not self.removedomethod(self.doaxes): return
2973 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
2974 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
2975 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
2976 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[2])
2977 # items = list(self.axes.items())
2978 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
2979 # for key, axis in items:
2980 # num = self.keynum(key)
2981 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
2982 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
2983 # if XPattern.match(key):
2984 # axis.vypos = 0
2985 # axis.vzpos = 0
2986 # axis._vtickpoint = self._vxtickpoint
2987 # axis.vgridpath = self.vxgridpath
2988 # axis.vbaseline = self.vxbaseline
2989 # axis.vtickdirection = self.vxtickdirection
2990 # elif YPattern.match(key):
2991 # axis.vxpos = 0
2992 # axis.vzpos = 0
2993 # axis._vtickpoint = self._vytickpoint
2994 # axis.vgridpath = self.vygridpath
2995 # axis.vbaseline = self.vybaseline
2996 # axis.vtickdirection = self.vytickdirection
2997 # elif ZPattern.match(key):
2998 # axis.vxpos = 0
2999 # axis.vypos = 0
3000 # axis._vtickpoint = self._vztickpoint
3001 # axis.vgridpath = self.vzgridpath
3002 # axis.vbaseline = self.vzbaseline
3003 # axis.vtickdirection = self.vztickdirection
3004 # else:
3005 # raise ValueError("Axis key '%s' not allowed" % key)
3006 # if axis.painter is not None:
3007 # axis.dopaint(self)
3008 # # if XPattern.match(key):
3009 # # self._xaxisextents[num2] += axis._extent
3010 # # needxaxisdist[num2] = 1
3011 # # if YPattern.match(key):
3012 # # self._yaxisextents[num2] += axis._extent
3013 # # needyaxisdist[num2] = 1
3015 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
3016 # phi=30, theta=30, distance=1,
3017 # backgroundattrs=None, axesdist="0.8 cm", **axes):
3018 # canvas.canvas.__init__(self)
3019 # self.tex = tex
3020 # self.xpos = xpos
3021 # self.ypos = ypos
3022 # self._xpos = unit.topt(xpos)
3023 # self._ypos = unit.topt(ypos)
3024 # self._width = unit.topt(width)
3025 # self._height = unit.topt(height)
3026 # self._depth = unit.topt(depth)
3027 # self.width = width
3028 # self.height = height
3029 # self.depth = depth
3030 # if self._width <= 0: raise ValueError("width < 0")
3031 # if self._height <= 0: raise ValueError("height < 0")
3032 # if self._depth <= 0: raise ValueError("height < 0")
3033 # self._distance = distance*math.sqrt(self._width*self._width+
3034 # self._height*self._height+
3035 # self._depth*self._depth)
3036 # phi *= -math.pi/180
3037 # theta *= math.pi/180
3038 # self.a = (-math.sin(phi), math.cos(phi), 0)
3039 # self.b = (-math.cos(phi)*math.sin(theta),
3040 # -math.sin(phi)*math.sin(theta),
3041 # math.cos(theta))
3042 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
3043 # self._distance*math.sin(phi)*math.cos(theta),
3044 # self._distance*math.sin(theta))
3045 # self.initaxes(axes)
3046 # self.axesdist_str = axesdist
3047 # self.backgroundattrs = backgroundattrs
3049 # self.data = []
3050 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
3051 # self.haslayout = 0
3052 # self.defaultstyle = {}
3054 # def bbox(self):
3055 # self.finish()
3056 # return bbox._bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
3059 ################################################################################
3060 # attr changers
3061 ################################################################################
3064 #class _Ichangeattr:
3065 # """attribute changer
3066 # is an iterator for attributes where an attribute
3067 # is not refered by just a number (like for a sequence),
3068 # but also by the number of attributes requested
3069 # by calls of the next method (like for an color palette)
3070 # (you should ensure to call all needed next before the attr)
3072 # the attribute itself is implemented by overloading the _attr method"""
3074 # def attr(self):
3075 # "get an attribute"
3077 # def next(self):
3078 # "get an attribute changer for the next attribute"
3081 class _changeattr: pass
3084 class changeattr(_changeattr):
3086 def __init__(self):
3087 self.counter = 1
3089 def getattr(self):
3090 return self.attr(0)
3092 def iterate(self):
3093 newindex = self.counter
3094 self.counter += 1
3095 return refattr(self, newindex)
3098 class refattr(_changeattr):
3100 def __init__(self, ref, index):
3101 self.ref = ref
3102 self.index = index
3104 def getattr(self):
3105 return self.ref.attr(self.index)
3107 def iterate(self):
3108 return self.ref.iterate()
3111 # helper routines for a using attrs
3113 def _getattr(attr):
3114 "get attr out of a attr/changeattr"
3115 if isinstance(attr, _changeattr):
3116 return attr.getattr()
3117 return attr
3120 def _getattrs(attrs):
3121 "get attrs out of a sequence of attr/changeattr"
3122 if attrs is not None:
3123 result = []
3124 for attr in helper.ensuresequence(attrs):
3125 if isinstance(attr, _changeattr):
3126 attr = attr.getattr()
3127 if attr is not None:
3128 result.append(attr)
3129 if len(result) or not len(attrs):
3130 return result
3133 def _iterateattr(attr):
3134 "perform next to a attr/changeattr"
3135 if isinstance(attr, _changeattr):
3136 return attr.iterate()
3137 return attr
3140 def _iterateattrs(attrs):
3141 "perform next to a sequence of attr/changeattr"
3142 if attrs is not None:
3143 result = []
3144 for attr in helper.ensuresequence(attrs):
3145 if isinstance(attr, _changeattr):
3146 result.append(attr.iterate())
3147 else:
3148 result.append(attr)
3149 return result
3152 class changecolor(changeattr):
3154 def __init__(self, palette):
3155 changeattr.__init__(self)
3156 self.palette = palette
3158 def attr(self, index):
3159 if self.counter != 1:
3160 return self.palette.getcolor(index/float(self.counter-1))
3161 else:
3162 return self.palette.getcolor(0)
3165 class _changecolorgray(changecolor):
3167 def __init__(self, palette=color.palette.Gray):
3168 changecolor.__init__(self, palette)
3170 _changecolorgrey = _changecolorgray
3173 class _changecolorreversegray(changecolor):
3175 def __init__(self, palette=color.palette.ReverseGray):
3176 changecolor.__init__(self, palette)
3178 _changecolorreversegrey = _changecolorreversegray
3181 class _changecolorredblack(changecolor):
3183 def __init__(self, palette=color.palette.RedBlack):
3184 changecolor.__init__(self, palette)
3187 class _changecolorblackred(changecolor):
3189 def __init__(self, palette=color.palette.BlackRed):
3190 changecolor.__init__(self, palette)
3193 class _changecolorredwhite(changecolor):
3195 def __init__(self, palette=color.palette.RedWhite):
3196 changecolor.__init__(self, palette)
3199 class _changecolorwhitered(changecolor):
3201 def __init__(self, palette=color.palette.WhiteRed):
3202 changecolor.__init__(self, palette)
3205 class _changecolorgreenblack(changecolor):
3207 def __init__(self, palette=color.palette.GreenBlack):
3208 changecolor.__init__(self, palette)
3211 class _changecolorblackgreen(changecolor):
3213 def __init__(self, palette=color.palette.BlackGreen):
3214 changecolor.__init__(self, palette)
3217 class _changecolorgreenwhite(changecolor):
3219 def __init__(self, palette=color.palette.GreenWhite):
3220 changecolor.__init__(self, palette)
3223 class _changecolorwhitegreen(changecolor):
3225 def __init__(self, palette=color.palette.WhiteGreen):
3226 changecolor.__init__(self, palette)
3229 class _changecolorblueblack(changecolor):
3231 def __init__(self, palette=color.palette.BlueBlack):
3232 changecolor.__init__(self, palette)
3235 class _changecolorblackblue(changecolor):
3237 def __init__(self, palette=color.palette.BlackBlue):
3238 changecolor.__init__(self, palette)
3241 class _changecolorbluewhite(changecolor):
3243 def __init__(self, palette=color.palette.BlueWhite):
3244 changecolor.__init__(self, palette)
3247 class _changecolorwhiteblue(changecolor):
3249 def __init__(self, palette=color.palette.WhiteBlue):
3250 changecolor.__init__(self, palette)
3253 class _changecolorredgreen(changecolor):
3255 def __init__(self, palette=color.palette.RedGreen):
3256 changecolor.__init__(self, palette)
3259 class _changecolorredblue(changecolor):
3261 def __init__(self, palette=color.palette.RedBlue):
3262 changecolor.__init__(self, palette)
3265 class _changecolorgreenred(changecolor):
3267 def __init__(self, palette=color.palette.GreenRed):
3268 changecolor.__init__(self, palette)
3271 class _changecolorgreenblue(changecolor):
3273 def __init__(self, palette=color.palette.GreenBlue):
3274 changecolor.__init__(self, palette)
3277 class _changecolorbluered(changecolor):
3279 def __init__(self, palette=color.palette.BlueRed):
3280 changecolor.__init__(self, palette)
3283 class _changecolorbluegreen(changecolor):
3285 def __init__(self, palette=color.palette.BlueGreen):
3286 changecolor.__init__(self, palette)
3289 class _changecolorrainbow(changecolor):
3291 def __init__(self, palette=color.palette.Rainbow):
3292 changecolor.__init__(self, palette)
3295 class _changecolorreverserainbow(changecolor):
3297 def __init__(self, palette=color.palette.ReverseRainbow):
3298 changecolor.__init__(self, palette)
3301 class _changecolorhue(changecolor):
3303 def __init__(self, palette=color.palette.Hue):
3304 changecolor.__init__(self, palette)
3307 class _changecolorreversehue(changecolor):
3309 def __init__(self, palette=color.palette.ReverseHue):
3310 changecolor.__init__(self, palette)
3313 changecolor.Gray = _changecolorgray
3314 changecolor.Grey = _changecolorgrey
3315 changecolor.Reversegray = _changecolorreversegray
3316 changecolor.Reversegrey = _changecolorreversegrey
3317 changecolor.RedBlack = _changecolorredblack
3318 changecolor.BlackRed = _changecolorblackred
3319 changecolor.RedWhite = _changecolorredwhite
3320 changecolor.WhiteRed = _changecolorwhitered
3321 changecolor.GreenBlack = _changecolorgreenblack
3322 changecolor.BlackGreen = _changecolorblackgreen
3323 changecolor.GreenWhite = _changecolorgreenwhite
3324 changecolor.WhiteGreen = _changecolorwhitegreen
3325 changecolor.BlueBlack = _changecolorblueblack
3326 changecolor.BlackBlue = _changecolorblackblue
3327 changecolor.BlueWhite = _changecolorbluewhite
3328 changecolor.WhiteBlue = _changecolorwhiteblue
3329 changecolor.RedGreen = _changecolorredgreen
3330 changecolor.RedBlue = _changecolorredblue
3331 changecolor.GreenRed = _changecolorgreenred
3332 changecolor.GreenBlue = _changecolorgreenblue
3333 changecolor.BlueRed = _changecolorbluered
3334 changecolor.BlueGreen = _changecolorbluegreen
3335 changecolor.Rainbow = _changecolorrainbow
3336 changecolor.ReverseRainbow = _changecolorreverserainbow
3337 changecolor.Hue = _changecolorhue
3338 changecolor.ReverseHue = _changecolorreversehue
3341 class changesequence(changeattr):
3342 "cycles through a sequence"
3344 def __init__(self, *sequence):
3345 changeattr.__init__(self)
3346 if not len(sequence):
3347 sequence = self.defaultsequence
3348 self.sequence = sequence
3350 def attr(self, index):
3351 return self.sequence[index % len(self.sequence)]
3354 class changelinestyle(changesequence):
3355 defaultsequence = (canvas.linestyle.solid,
3356 canvas.linestyle.dashed,
3357 canvas.linestyle.dotted,
3358 canvas.linestyle.dashdotted)
3361 class changestrokedfilled(changesequence):
3362 defaultsequence = (canvas.stroked(), canvas.filled())
3365 class changefilledstroked(changesequence):
3366 defaultsequence = (canvas.filled(), canvas.stroked())
3370 ################################################################################
3371 # styles
3372 ################################################################################
3375 class symbol:
3377 def cross(self, x, y):
3378 return (path._moveto(x-0.5*self._size, y-0.5*self._size),
3379 path._lineto(x+0.5*self._size, y+0.5*self._size),
3380 path._moveto(x-0.5*self._size, y+0.5*self._size),
3381 path._lineto(x+0.5*self._size, y-0.5*self._size))
3383 def plus(self, x, y):
3384 return (path._moveto(x-0.707106781*self._size, y),
3385 path._lineto(x+0.707106781*self._size, y),
3386 path._moveto(x, y-0.707106781*self._size),
3387 path._lineto(x, y+0.707106781*self._size))
3389 def square(self, x, y):
3390 return (path._moveto(x-0.5*self._size, y-0.5 * self._size),
3391 path._lineto(x+0.5*self._size, y-0.5 * self._size),
3392 path._lineto(x+0.5*self._size, y+0.5 * self._size),
3393 path._lineto(x-0.5*self._size, y+0.5 * self._size),
3394 path.closepath())
3396 def triangle(self, x, y):
3397 return (path._moveto(x-0.759835685*self._size, y-0.438691337*self._size),
3398 path._lineto(x+0.759835685*self._size, y-0.438691337*self._size),
3399 path._lineto(x, y+0.877382675*self._size),
3400 path.closepath())
3402 def circle(self, x, y):
3403 return (path._arc(x, y, 0.564189583*self._size, 0, 360),
3404 path.closepath())
3406 def diamond(self, x, y):
3407 return (path._moveto(x-0.537284965*self._size, y),
3408 path._lineto(x, y-0.930604859*self._size),
3409 path._lineto(x+0.537284965*self._size, y),
3410 path._lineto(x, y+0.930604859*self._size),
3411 path.closepath())
3413 def __init__(self, symbol=helper.nodefault,
3414 size="0.2 cm", symbolattrs=canvas.stroked(),
3415 errorscale=0.5, errorbarattrs=(),
3416 lineattrs=None):
3417 self.size_str = size
3418 if symbol is helper.nodefault:
3419 self._symbol = changesymbol.cross()
3420 else:
3421 self._symbol = symbol
3422 self._symbolattrs = symbolattrs
3423 self.errorscale = errorscale
3424 self._errorbarattrs = errorbarattrs
3425 self._lineattrs = lineattrs
3427 def iteratedict(self):
3428 result = {}
3429 result["symbol"] = _iterateattr(self._symbol)
3430 result["size"] = _iterateattr(self.size_str)
3431 result["symbolattrs"] = _iterateattrs(self._symbolattrs)
3432 result["errorscale"] = _iterateattr(self.errorscale)
3433 result["errorbarattrs"] = _iterateattrs(self._errorbarattrs)
3434 result["lineattrs"] = _iterateattrs(self._lineattrs)
3435 return result
3437 def iterate(self):
3438 return symbol(**self.iteratedict())
3440 def othercolumnkey(self, key, index):
3441 raise ValueError("unsuitable key '%s'" % key)
3443 def setcolumns(self, graph, columns):
3444 def checkpattern(key, index, pattern, iskey, isindex):
3445 if key is not None:
3446 match = pattern.match(key)
3447 if match:
3448 if isindex is not None: raise ValueError("multiple key specification")
3449 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
3450 key = None
3451 iskey = match.groups()[0]
3452 isindex = index
3453 return key, iskey, isindex
3455 self.xi = self.xmini = self.xmaxi = None
3456 self.dxi = self.dxmini = self.dxmaxi = None
3457 self.yi = self.ymini = self.ymaxi = None
3458 self.dyi = self.dymini = self.dymaxi = None
3459 self.xkey = self.ykey = None
3460 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
3461 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3462 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3463 XMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
3464 YMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
3465 XMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
3466 YMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
3467 DXPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3468 DYPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3469 DXMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
3470 DYMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
3471 DXMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
3472 DYMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
3473 for key, index in columns.items():
3474 key, self.xkey, self.xi = checkpattern(key, index, XPattern, self.xkey, self.xi)
3475 key, self.ykey, self.yi = checkpattern(key, index, YPattern, self.ykey, self.yi)
3476 key, self.xkey, self.xmini = checkpattern(key, index, XMinPattern, self.xkey, self.xmini)
3477 key, self.ykey, self.ymini = checkpattern(key, index, YMinPattern, self.ykey, self.ymini)
3478 key, self.xkey, self.xmaxi = checkpattern(key, index, XMaxPattern, self.xkey, self.xmaxi)
3479 key, self.ykey, self.ymaxi = checkpattern(key, index, YMaxPattern, self.ykey, self.ymaxi)
3480 key, self.xkey, self.dxi = checkpattern(key, index, DXPattern, self.xkey, self.dxi)
3481 key, self.ykey, self.dyi = checkpattern(key, index, DYPattern, self.ykey, self.dyi)
3482 key, self.xkey, self.dxmini = checkpattern(key, index, DXMinPattern, self.xkey, self.dxmini)
3483 key, self.ykey, self.dymini = checkpattern(key, index, DYMinPattern, self.ykey, self.dymini)
3484 key, self.xkey, self.dxmaxi = checkpattern(key, index, DXMaxPattern, self.xkey, self.dxmaxi)
3485 key, self.ykey, self.dymaxi = checkpattern(key, index, DYMaxPattern, self.ykey, self.dymaxi)
3486 if key is not None:
3487 self.othercolumnkey(key, index)
3488 if None in (self.xkey, self.ykey): raise ValueError("incomplete axis specification")
3489 if (len(filter(None, (self.xmini, self.dxmini, self.dxi))) > 1 or
3490 len(filter(None, (self.ymini, self.dymini, self.dyi))) > 1 or
3491 len(filter(None, (self.xmaxi, self.dxmaxi, self.dxi))) > 1 or
3492 len(filter(None, (self.ymaxi, self.dymaxi, self.dyi))) > 1):
3493 raise ValueError("multiple errorbar definition")
3494 if ((self.xi is None and self.dxi is not None) or
3495 (self.yi is None and self.dyi is not None) or
3496 (self.xi is None and self.dxmini is not None) or
3497 (self.yi is None and self.dymini is not None) or
3498 (self.xi is None and self.dxmaxi is not None) or
3499 (self.yi is None and self.dymaxi is not None)):
3500 raise ValueError("errorbar definition start value missing")
3501 self.xaxis = graph.axes[self.xkey]
3502 self.yaxis = graph.axes[self.ykey]
3504 def minmidmax(self, point, i, mini, maxi, di, dmini, dmaxi):
3505 min = max = mid = None
3506 try:
3507 mid = point[i] + 0.0
3508 except (TypeError, ValueError):
3509 pass
3510 try:
3511 if di is not None: min = point[i] - point[di]
3512 elif dmini is not None: min = point[i] - point[dmini]
3513 elif mini is not None: min = point[mini] + 0.0
3514 except (TypeError, ValueError):
3515 pass
3516 try:
3517 if di is not None: max = point[i] + point[di]
3518 elif dmaxi is not None: max = point[i] + point[dmaxi]
3519 elif maxi is not None: max = point[maxi] + 0.0
3520 except (TypeError, ValueError):
3521 pass
3522 if mid is not None:
3523 if min is not None and min > mid: raise ValueError("minimum error in errorbar")
3524 if max is not None and max < mid: raise ValueError("maximum error in errorbar")
3525 else:
3526 if min is not None and max is not None and min > max: raise ValueError("minimum/maximum error in errorbar")
3527 return min, mid, max
3529 def keyrange(self, points, i, mini, maxi, di, dmini, dmaxi):
3530 allmin = allmax = None
3531 if filter(None, (mini, maxi, di, dmini, dmaxi)) is not None:
3532 for point in points:
3533 min, mid, max = self.minmidmax(point, i, mini, maxi, di, dmini, dmaxi)
3534 if min is not None and (allmin is None or min < allmin): allmin = min
3535 if mid is not None and (allmin is None or mid < allmin): allmin = mid
3536 if mid is not None and (allmax is None or mid > allmax): allmax = mid
3537 if max is not None and (allmax is None or max > allmax): allmax = max
3538 else:
3539 for point in points:
3540 try:
3541 value = point[i] + 0.0
3542 if allmin is None or point[i] < allmin: allmin = point[i]
3543 if allmax is None or point[i] > allmax: allmax = point[i]
3544 except (TypeError, ValueError):
3545 pass
3546 return allmin, allmax
3548 def getranges(self, points):
3549 xmin, xmax = self.keyrange(points, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
3550 ymin, ymax = self.keyrange(points, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
3551 return {self.xkey: (xmin, xmax), self.ykey: (ymin, ymax)}
3553 def _drawerrorbar(self, graph, topleft, top, topright,
3554 left, center, right,
3555 bottomleft, bottom, bottomright, point=None):
3556 if left is not None:
3557 if right is not None:
3558 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3559 left2 = graph._addpos(*(left+(0, self._errorsize)))
3560 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3561 right2 = graph._addpos(*(right+(0, self._errorsize)))
3562 graph.stroke(path.path(path._moveto(*left1),
3563 graph._connect(*(left1+left2)),
3564 path._moveto(*left),
3565 graph._connect(*(left+right)),
3566 path._moveto(*right1),
3567 graph._connect(*(right1+right2))),
3568 *self.errorbarattrs)
3569 elif center is not None:
3570 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3571 left2 = graph._addpos(*(left+(0, self._errorsize)))
3572 graph.stroke(path.path(path._moveto(*left1),
3573 graph._connect(*(left1+left2)),
3574 path._moveto(*left),
3575 graph._connect(*(left+center))),
3576 *self.errorbarattrs)
3577 else:
3578 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3579 left2 = graph._addpos(*(left+(0, self._errorsize)))
3580 left3 = graph._addpos(*(left+(self._errorsize, 0)))
3581 graph.stroke(path.path(path._moveto(*left1),
3582 graph._connect(*(left1+left2)),
3583 path._moveto(*left),
3584 graph._connect(*(left+left3))),
3585 *self.errorbarattrs)
3586 if right is not None and left is None:
3587 if center is not None:
3588 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3589 right2 = graph._addpos(*(right+(0, self._errorsize)))
3590 graph.stroke(path.path(path._moveto(*right1),
3591 graph._connect(*(right1+right2)),
3592 path._moveto(*right),
3593 graph._connect(*(right+center))),
3594 *self.errorbarattrs)
3595 else:
3596 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3597 right2 = graph._addpos(*(right+(0, self._errorsize)))
3598 right3 = graph._addpos(*(right+(-self._errorsize, 0)))
3599 graph.stroke(path.path(path._moveto(*right1),
3600 graph._connect(*(right1+right2)),
3601 path._moveto(*right),
3602 graph._connect(*(right+right3))),
3603 *self.errorbarattrs)
3605 if bottom is not None:
3606 if top is not None:
3607 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3608 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3609 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3610 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3611 graph.stroke(path.path(path._moveto(*bottom1),
3612 graph._connect(*(bottom1+bottom2)),
3613 path._moveto(*bottom),
3614 graph._connect(*(bottom+top)),
3615 path._moveto(*top1),
3616 graph._connect(*(top1+top2))),
3617 *self.errorbarattrs)
3618 elif center is not None:
3619 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3620 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3621 graph.stroke(path.path(path._moveto(*bottom1),
3622 graph._connect(*(bottom1+bottom2)),
3623 path._moveto(*bottom),
3624 graph._connect(*(bottom+center))),
3625 *self.errorbarattrs)
3626 else:
3627 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3628 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3629 bottom3 = graph._addpos(*(bottom+(0, self._errorsize)))
3630 graph.stroke(path.path(path._moveto(*bottom1),
3631 graph._connect(*(bottom1+bottom2)),
3632 path._moveto(*bottom),
3633 graph._connect(*(bottom+bottom3))),
3634 *self.errorbarattrs)
3635 if top is not None and bottom is None:
3636 if center is not None:
3637 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3638 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3639 graph.stroke(path.path(path._moveto(*top1),
3640 graph._connect(*(top1+top2)),
3641 path._moveto(*top),
3642 graph._connect(*(top+center))),
3643 *self.errorbarattrs)
3644 else:
3645 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3646 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3647 top3 = graph._addpos(*(top+(0, -self._errorsize)))
3648 graph.stroke(path.path(path._moveto(*top1),
3649 graph._connect(*(top1+top2)),
3650 path._moveto(*top),
3651 graph._connect(*(top+top3))),
3652 *self.errorbarattrs)
3653 if bottomleft is not None:
3654 if topleft is not None and bottomright is None:
3655 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
3656 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
3657 graph.stroke(path.path(path._moveto(*bottomleft1),
3658 graph._connect(*(bottomleft1+bottomleft)),
3659 graph._connect(*(bottomleft+topleft)),
3660 graph._connect(*(topleft+topleft1))),
3661 *self.errorbarattrs)
3662 elif bottomright is not None and topleft is None:
3663 bottomleft1 = graph._addpos(*(bottomleft+(0, self._errorsize)))
3664 bottomright1 = graph._addpos(*(bottomright+(0, self._errorsize)))
3665 graph.stroke(path.path(path._moveto(*bottomleft1),
3666 graph._connect(*(bottomleft1+bottomleft)),
3667 graph._connect(*(bottomleft+bottomright)),
3668 graph._connect(*(bottomright+bottomright1))),
3669 *self.errorbarattrs)
3670 elif bottomright is None and topleft is None:
3671 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
3672 bottomleft2 = graph._addpos(*(bottomleft+(0, self._errorsize)))
3673 graph.stroke(path.path(path._moveto(*bottomleft1),
3674 graph._connect(*(bottomleft1+bottomleft)),
3675 graph._connect(*(bottomleft+bottomleft2))),
3676 *self.errorbarattrs)
3677 if topright is not None:
3678 if bottomright is not None and topleft is None:
3679 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
3680 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
3681 graph.stroke(path.path(path._moveto(*topright1),
3682 graph._connect(*(topright1+topright)),
3683 graph._connect(*(topright+bottomright)),
3684 graph._connect(*(bottomright+bottomright1))),
3685 *self.errorbarattrs)
3686 elif topleft is not None and bottomright is None:
3687 topright1 = graph._addpos(*(topright+(0, -self._errorsize)))
3688 topleft1 = graph._addpos(*(topleft+(0, -self._errorsize)))
3689 graph.stroke(path.path(path._moveto(*topright1),
3690 graph._connect(*(topright1+topright)),
3691 graph._connect(*(topright+topleft)),
3692 graph._connect(*(topleft+topleft1))),
3693 *self.errorbarattrs)
3694 elif topleft is None and bottomright is None:
3695 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
3696 topright2 = graph._addpos(*(topright+(0, -self._errorsize)))
3697 graph.stroke(path.path(path._moveto(*topright1),
3698 graph._connect(*(topright1+topright)),
3699 graph._connect(*(topright+topright2))),
3700 *self.errorbarattrs)
3701 if bottomright is not None and bottomleft is None and topright is None:
3702 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
3703 bottomright2 = graph._addpos(*(bottomright+(0, self._errorsize)))
3704 graph.stroke(path.path(path._moveto(*bottomright1),
3705 graph._connect(*(bottomright1+bottomright)),
3706 graph._connect(*(bottomright+bottomright2))),
3707 *self.errorbarattrs)
3708 if topleft is not None and bottomleft is None and topright is None:
3709 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
3710 topleft2 = graph._addpos(*(topleft+(0, -self._errorsize)))
3711 graph.stroke(path.path(path._moveto(*topleft1),
3712 graph._connect(*(topleft1+topleft)),
3713 graph._connect(*(topleft+topleft2))),
3714 *self.errorbarattrs)
3715 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
3716 graph.stroke(path.path(path._moveto(*bottomleft),
3717 graph._connect(*(bottomleft+bottomright)),
3718 graph._connect(*(bottomright+topright)),
3719 graph._connect(*(topright+topleft)),
3720 path.closepath()),
3721 *self.errorbarattrs)
3723 def _drawsymbol(self, canvas, x, y, point=None):
3724 canvas.draw(path.path(*self.symbol(self, x, y)), *self.symbolattrs)
3726 def drawsymbol(self, canvas, x, y, point=None):
3727 self._drawsymbol(canvas, unit.topt(x), unit.topt(y), point)
3729 def key(self, c, x, y, width, height):
3730 if self._symbolattrs is not None:
3731 self._drawsymbol(c, x + 0.5 * width, y + 0.5 * height)
3732 if self._lineattrs is not None:
3733 c.stroke(path._line(x, y + 0.5 * height, x + width, y + 0.5 * height), *self.lineattrs)
3735 def drawpoints(self, graph, points):
3736 xaxismin, xaxismax = self.xaxis.getdatarange()
3737 yaxismin, yaxismax = self.yaxis.getdatarange()
3738 self.size = unit.length(_getattr(self.size_str), default_type="v")
3739 self._size = unit.topt(self.size)
3740 self.symbol = _getattr(self._symbol)
3741 self.symbolattrs = _getattrs(helper.ensuresequence(self._symbolattrs))
3742 self.errorbarattrs = _getattrs(helper.ensuresequence(self._errorbarattrs))
3743 self._errorsize = self.errorscale * self._size
3744 self.errorsize = self.errorscale * self.size
3745 self.lineattrs = _getattrs(helper.ensuresequence(self._lineattrs))
3746 if self._lineattrs is not None:
3747 clipcanvas = graph.clipcanvas()
3748 lineels = []
3749 haserror = filter(None, (self.xmini, self.ymini, self.xmaxi, self.ymaxi,
3750 self.dxi, self.dyi, self.dxmini, self.dymini, self.dxmaxi, self.dymaxi)) is not None
3751 moveto = 1
3752 for point in points:
3753 drawsymbol = 1
3754 xmin, x, xmax = self.minmidmax(point, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
3755 ymin, y, ymax = self.minmidmax(point, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
3756 if x is not None and x < xaxismin: drawsymbol = 0
3757 elif x is not None and x > xaxismax: drawsymbol = 0
3758 elif y is not None and y < yaxismin: drawsymbol = 0
3759 elif y is not None and y > yaxismax: drawsymbol = 0
3760 elif haserror:
3761 if xmin is not None and xmin < xaxismin: drawsymbol = 0
3762 elif xmax is not None and xmax < xaxismin: drawsymbol = 0
3763 elif xmax is not None and xmax > xaxismax: drawsymbol = 0
3764 elif xmin is not None and xmin > xaxismax: drawsymbol = 0
3765 elif ymin is not None and ymin < yaxismin: drawsymbol = 0
3766 elif ymax is not None and ymax < yaxismin: drawsymbol = 0
3767 elif ymax is not None and ymax > yaxismax: drawsymbol = 0
3768 elif ymin is not None and ymin > yaxismax: drawsymbol = 0
3769 xpos=ypos=topleft=top=topright=left=center=right=bottomleft=bottom=bottomright=None
3770 if x is not None and y is not None:
3771 try:
3772 center = xpos, ypos = graph._pos(x, y, xaxis=self.xaxis, yaxis=self.yaxis)
3773 except (ValueError, OverflowError): # XXX: exceptions???
3774 pass
3775 if haserror:
3776 if y is not None:
3777 if xmin is not None: left = graph._pos(xmin, y, xaxis=self.xaxis, yaxis=self.yaxis)
3778 if xmax is not None: right = graph._pos(xmax, y, xaxis=self.xaxis, yaxis=self.yaxis)
3779 if x is not None:
3780 if ymax is not None: top = graph._pos(x, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3781 if ymin is not None: bottom = graph._pos(x, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3782 if x is None or y is None:
3783 if ymax is not None:
3784 if xmin is not None: topleft = graph._pos(xmin, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3785 if xmax is not None: topright = graph._pos(xmax, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3786 if ymin is not None:
3787 if xmin is not None: bottomleft = graph._pos(xmin, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3788 if xmax is not None: bottomright = graph._pos(xmax, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3789 if drawsymbol:
3790 if self._errorbarattrs is not None and haserror:
3791 self._drawerrorbar(graph, topleft, top, topright,
3792 left, center, right,
3793 bottomleft, bottom, bottomright, point)
3794 if self._symbolattrs is not None and xpos is not None and ypos is not None:
3795 self._drawsymbol(graph, xpos, ypos, point)
3796 if xpos is not None and ypos is not None:
3797 if moveto:
3798 lineels.append(path._moveto(xpos, ypos))
3799 moveto = 0
3800 else:
3801 lineels.append(path._lineto(xpos, ypos))
3802 else:
3803 moveto = 1
3804 self.path = path.path(*lineels)
3805 if self._lineattrs is not None:
3806 clipcanvas.stroke(self.path, *self.lineattrs)
3809 class changesymbol(changesequence): pass
3812 class _changesymbolcross(changesymbol):
3813 defaultsequence = (symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond)
3816 class _changesymbolplus(changesymbol):
3817 defaultsequence = (symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross)
3820 class _changesymbolsquare(changesymbol):
3821 defaultsequence = (symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus)
3824 class _changesymboltriangle(changesymbol):
3825 defaultsequence = (symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square)
3828 class _changesymbolcircle(changesymbol):
3829 defaultsequence = (symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle)
3832 class _changesymboldiamond(changesymbol):
3833 defaultsequence = (symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle)
3836 class _changesymbolsquaretwice(changesymbol):
3837 defaultsequence = (symbol.square, symbol.square, symbol.triangle, symbol.triangle,
3838 symbol.circle, symbol.circle, symbol.diamond, symbol.diamond)
3841 class _changesymboltriangletwice(changesymbol):
3842 defaultsequence = (symbol.triangle, symbol.triangle, symbol.circle, symbol.circle,
3843 symbol.diamond, symbol.diamond, symbol.square, symbol.square)
3846 class _changesymbolcircletwice(changesymbol):
3847 defaultsequence = (symbol.circle, symbol.circle, symbol.diamond, symbol.diamond,
3848 symbol.square, symbol.square, symbol.triangle, symbol.triangle)
3851 class _changesymboldiamondtwice(changesymbol):
3852 defaultsequence = (symbol.diamond, symbol.diamond, symbol.square, symbol.square,
3853 symbol.triangle, symbol.triangle, symbol.circle, symbol.circle)
3856 changesymbol.cross = _changesymbolcross
3857 changesymbol.plus = _changesymbolplus
3858 changesymbol.square = _changesymbolsquare
3859 changesymbol.triangle = _changesymboltriangle
3860 changesymbol.circle = _changesymbolcircle
3861 changesymbol.diamond = _changesymboldiamond
3862 changesymbol.squaretwice = _changesymbolsquaretwice
3863 changesymbol.triangletwice = _changesymboltriangletwice
3864 changesymbol.circletwice = _changesymbolcircletwice
3865 changesymbol.diamondtwice = _changesymboldiamondtwice
3868 class line(symbol):
3870 def __init__(self, lineattrs=helper.nodefault):
3871 if lineattrs is helper.nodefault:
3872 lineattrs = (changelinestyle(), canvas.linejoin.round)
3873 symbol.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
3876 class rect(symbol):
3878 def __init__(self, palette=color.palette.Gray):
3879 self.palette = palette
3880 self.colorindex = None
3881 symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
3883 def iterate(self):
3884 raise RuntimeError("style is not iterateable")
3886 def othercolumnkey(self, key, index):
3887 if key == "color":
3888 self.colorindex = index
3889 else:
3890 symbol.othercolumnkey(self, key, index)
3892 def _drawerrorbar(self, graph, topleft, top, topright,
3893 left, center, right,
3894 bottomleft, bottom, bottomright, point=None):
3895 color = point[self.colorindex]
3896 if color is not None:
3897 if color != self.lastcolor:
3898 self.rectclipcanvas.set(self.palette.getcolor(color))
3899 if bottom is not None and left is not None:
3900 bottomleft = left[0], bottom[1]
3901 if bottom is not None and right is not None:
3902 bottomright = right[0], bottom[1]
3903 if top is not None and right is not None:
3904 topright = right[0], top[1]
3905 if top is not None and left is not None:
3906 topleft = left[0], top[1]
3907 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
3908 self.rectclipcanvas.fill(path.path(path._moveto(*bottomleft),
3909 graph._connect(*(bottomleft+bottomright)),
3910 graph._connect(*(bottomright+topright)),
3911 graph._connect(*(topright+topleft)),
3912 path.closepath()))
3914 def drawpoints(self, graph, points):
3915 if self.colorindex is None:
3916 raise RuntimeError("column 'color' not set")
3917 self.lastcolor = None
3918 self.rectclipcanvas = graph.clipcanvas()
3919 symbol.drawpoints(self, graph, points)
3921 def key(self, c, x, y, width, height):
3922 raise RuntimeError("style doesn't yet provide a key")
3925 class text(symbol):
3927 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=textmodule.halign.center, **args):
3928 self.textindex = None
3929 self.textdx_str = textdx
3930 self.textdy_str = textdy
3931 self._textattrs = textattrs
3932 symbol.__init__(self, **args)
3934 def iteratedict(self):
3935 result = symbol.iteratedict()
3936 result["textattrs"] = _iterateattr(self._textattrs)
3937 return result
3939 def iterate(self):
3940 return textsymbol(**self.iteratedict())
3942 def othercolumnkey(self, key, index):
3943 if key == "text":
3944 self.textindex = index
3945 else:
3946 symbol.othercolumnkey(self, key, index)
3948 def _drawsymbol(self, graph, x, y, point=None):
3949 symbol._drawsymbol(self, graph, x, y, point)
3950 if None not in (x, y, point[self.textindex]) and self._textattrs is not None:
3951 graph._text(x + self._textdx, y + self._textdy, str(point[self.textindex]), *helper.ensuresequence(self.textattrs))
3953 def drawpoints(self, graph, points):
3954 self.textdx = unit.length(_getattr(self.textdx_str), default_type="v")
3955 self.textdy = unit.length(_getattr(self.textdy_str), default_type="v")
3956 self._textdx = unit.topt(self.textdx)
3957 self._textdy = unit.topt(self.textdy)
3958 if self._textattrs is not None:
3959 self.textattrs = _getattr(self._textattrs)
3960 if self.textindex is None:
3961 raise RuntimeError("column 'text' not set")
3962 symbol.drawpoints(self, graph, points)
3964 def key(self, c, x, y, width, height):
3965 raise RuntimeError("style doesn't yet provide a key")
3968 class arrow(symbol):
3970 def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
3971 self.linelength_str = linelength
3972 self.arrowsize_str = arrowsize
3973 self.arrowattrs = arrowattrs
3974 self.arrowdict = arrowdict
3975 self.epsilon = epsilon
3976 self.sizeindex = self.angleindex = None
3977 symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
3979 def iterate(self):
3980 raise RuntimeError("style is not iterateable")
3982 def othercolumnkey(self, key, index):
3983 if key == "size":
3984 self.sizeindex = index
3985 elif key == "angle":
3986 self.angleindex = index
3987 else:
3988 symbol.othercolumnkey(self, key, index)
3990 def _drawsymbol(self, graph, x, y, point=None):
3991 if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
3992 if point[self.sizeindex] > self.epsilon:
3993 dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
3994 x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
3995 y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
3996 x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
3997 y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
3998 graph.stroke(path.line(x1, y1, x2, y2),
3999 canvas.earrow(self.arrowsize*point[self.sizeindex],
4000 **self.arrowdict),
4001 *helper.ensuresequence(self.arrowattrs))
4003 def drawpoints(self, graph, points):
4004 self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
4005 self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
4006 self._arrowsize = unit.topt(self.arrowsize)
4007 self._linelength = unit.topt(self.linelength)
4008 if self.sizeindex is None:
4009 raise RuntimeError("column 'size' not set")
4010 if self.angleindex is None:
4011 raise RuntimeError("column 'angle' not set")
4012 symbol.drawpoints(self, graph, points)
4014 def key(self, c, x, y, width, height):
4015 raise RuntimeError("style doesn't yet provide a key")
4018 class _bariterator(changeattr):
4020 def attr(self, index):
4021 return index, self.counter
4024 class bar:
4026 def __init__(self, fromzero=1, stacked=0, skipmissing=1, xbar=0,
4027 barattrs=helper.nodefault, _usebariterator=helper.nodefault, _previousbar=None):
4028 self.fromzero = fromzero
4029 self.stacked = stacked
4030 self.skipmissing = skipmissing
4031 self.xbar = xbar
4032 if barattrs is helper.nodefault:
4033 self._barattrs = (canvas.stroked(color.gray.black), changecolor.Rainbow())
4034 else:
4035 self._barattrs = barattrs
4036 if _usebariterator is helper.nodefault:
4037 self.bariterator = _bariterator()
4038 else:
4039 self.bariterator = _usebariterator
4040 self.previousbar = _previousbar
4042 def iteratedict(self):
4043 result = {}
4044 result["barattrs"] = _iterateattrs(self._barattrs)
4045 return result
4047 def iterate(self):
4048 return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
4049 _usebariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
4051 def setcolumns(self, graph, columns):
4052 def checkpattern(key, index, pattern, iskey, isindex):
4053 if key is not None:
4054 match = pattern.match(key)
4055 if match:
4056 if isindex is not None: raise ValueError("multiple key specification")
4057 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4058 key = None
4059 iskey = match.groups()[0]
4060 isindex = index
4061 return key, iskey, isindex
4063 xkey = ykey = None
4064 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4065 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4066 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4067 xi = yi = None
4068 for key, index in columns.items():
4069 key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
4070 key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
4071 if key is not None:
4072 self.othercolumnkey(key, index)
4073 if None in (xkey, ykey): raise ValueError("incomplete axis specification")
4074 if self.xbar:
4075 self.nkey, self.ni = ykey, yi
4076 self.vkey, self.vi = xkey, xi
4077 else:
4078 self.nkey, self.ni = xkey, xi
4079 self.vkey, self.vi = ykey, yi
4080 self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
4082 def getranges(self, points):
4083 index, count = _getattr(self.bariterator)
4084 if count != 1 and self.stacked != 1:
4085 if self.stacked > 1:
4086 index = divmod(index, self.stacked)[0]
4088 vmin = vmax = None
4089 for point in points:
4090 if not self.skipmissing:
4091 if count != 1 and self.stacked != 1:
4092 self.naxis.setname(point[self.ni], index)
4093 else:
4094 self.naxis.setname(point[self.ni])
4095 try:
4096 v = point[self.vi] + 0.0
4097 if vmin is None or v < vmin: vmin = v
4098 if vmax is None or v > vmax: vmax = v
4099 except (TypeError, ValueError):
4100 pass
4101 else:
4102 if self.skipmissing:
4103 if count != 1 and self.stacked != 1:
4104 self.naxis.setname(point[self.ni], index)
4105 else:
4106 self.naxis.setname(point[self.ni])
4107 if self.fromzero:
4108 if vmin > 0: vmin = 0
4109 if vmax < 0: vmax = 0
4110 return {self.vkey: (vmin, vmax)}
4112 def drawpoints(self, graph, points):
4113 index, count = _getattr(self.bariterator)
4114 dostacked = (self.stacked != 0 and
4115 (self.stacked == 1 or divmod(index, self.stacked)[1]) and
4116 (self.stacked != 1 or index))
4117 if self.stacked > 1:
4118 index = divmod(index, self.stacked)[0]
4119 vmin, vmax = self.vaxis.getdatarange()
4120 self.barattrs = _getattrs(helper.ensuresequence(self._barattrs))
4121 if self.stacked:
4122 self.stackedvalue = {}
4123 for point in points:
4124 try:
4125 n = point[self.ni]
4126 v = point[self.vi]
4127 if self.stacked:
4128 self.stackedvalue[n] = v
4129 if count != 1 and self.stacked != 1:
4130 minid = (n, index, 0)
4131 maxid = (n, index, 1)
4132 else:
4133 minid = (n, 0)
4134 maxid = (n, 1)
4135 if self.xbar:
4136 x1pos, y1pos = graph._pos(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
4137 x2pos, y2pos = graph._pos(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4138 else:
4139 x1pos, y1pos = graph._pos(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
4140 x2pos, y2pos = graph._pos(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
4141 if dostacked:
4142 if self.xbar:
4143 x3pos, y3pos = graph._pos(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
4144 x4pos, y4pos = graph._pos(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
4145 else:
4146 x3pos, y3pos = graph._pos(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4147 x4pos, y4pos = graph._pos(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4148 else:
4149 if self.fromzero:
4150 if self.xbar:
4151 x3pos, y3pos = graph._pos(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4152 x4pos, y4pos = graph._pos(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
4153 else:
4154 x3pos, y3pos = graph._pos(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4155 x4pos, y4pos = graph._pos(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4156 else:
4157 x3pos, y3pos = self.naxis._vtickpoint(self.naxis, self.naxis.convert(maxid))
4158 x4pos, y4pos = self.naxis._vtickpoint(self.naxis, self.naxis.convert(minid))
4159 if self.barattrs is not None:
4160 graph.fill(path.path(path._moveto(x1pos, y1pos),
4161 graph._connect(x1pos, y1pos, x2pos, y2pos),
4162 graph._connect(x2pos, y2pos, x3pos, y3pos),
4163 graph._connect(x3pos, y3pos, x4pos, y4pos),
4164 graph._connect(x4pos, y4pos, x1pos, y1pos), # no closepath (might not be straight)
4165 path.closepath()), *self.barattrs)
4166 except (TypeError, ValueError): pass
4168 def key(self, c, x, y, width, height):
4169 c.fill(path._rect(x, y, width, height), *self.barattrs)
4172 #class surface:
4174 # def setcolumns(self, graph, columns):
4175 # self.columns = columns
4177 # def getranges(self, points):
4178 # return {"x": (0, 10), "y": (0, 10), "z": (0, 1)}
4180 # def drawpoints(self, graph, points):
4181 # pass
4185 ################################################################################
4186 # data
4187 ################################################################################
4190 class data:
4192 defaultstyle = symbol
4194 def __init__(self, file, title=helper.nodefault, context={}, **columns):
4195 self.title = title
4196 if helper.isstring(file):
4197 self.data = datamodule.datafile(file)
4198 else:
4199 self.data = file
4200 if title is helper.nodefault:
4201 self.title = "(unknown)"
4202 else:
4203 self.title = title
4204 self.columns = {}
4205 for key, column in columns.items():
4206 try:
4207 self.columns[key] = self.data.getcolumnno(column)
4208 except datamodule.ColumnError:
4209 self.columns[key] = len(self.data.titles)
4210 self.data.addcolumn(column, context=context)
4212 def setstyle(self, graph, style):
4213 self.style = style
4214 self.style.setcolumns(graph, self.columns)
4216 def getranges(self):
4217 return self.style.getranges(self.data.data)
4219 def setranges(self, ranges):
4220 pass
4222 def draw(self, graph):
4223 self.style.drawpoints(graph, self.data.data)
4226 class function:
4228 defaultstyle = line
4230 def __init__(self, expression, title=helper.nodefault, min=None, max=None, points=100, parser=mathtree.parser(), context={}):
4231 if title is helper.nodefault:
4232 self.title = expression
4233 else:
4234 self.title = title
4235 self.min = min
4236 self.max = max
4237 self.points = points
4238 self.context = context
4239 self.result, expression = expression.split("=")
4240 self.mathtree = parser.parse(expression)
4241 self.variable = None
4242 self.evalranges = 0
4244 def setstyle(self, graph, style):
4245 for variable in self.mathtree.VarList():
4246 if variable in graph.axes.keys():
4247 if self.variable is None:
4248 self.variable = variable
4249 else:
4250 raise ValueError("multiple variables found")
4251 if self.variable is None:
4252 raise ValueError("no variable found")
4253 self.xaxis = graph.axes[self.variable]
4254 self.style = style
4255 self.style.setcolumns(graph, {self.variable: 0, self.result: 1})
4257 def getranges(self):
4258 if self.evalranges:
4259 return self.style.getranges(self.data)
4260 if None not in (self.min, self.max):
4261 return {self.variable: (self.min, self.max)}
4263 def setranges(self, ranges):
4264 if ranges.has_key(self.variable):
4265 min, max = ranges[self.variable]
4266 if self.min is not None: min = self.min
4267 if self.max is not None: max = self.max
4268 vmin = self.xaxis.convert(min)
4269 vmax = self.xaxis.convert(max)
4270 self.data = []
4271 for i in range(self.points):
4272 self.context[self.variable] = x = self.xaxis.invert(vmin + (vmax-vmin)*i / (self.points-1.0))
4273 try:
4274 y = self.mathtree.Calc(**self.context)
4275 except (ArithmeticError, ValueError):
4276 y = None
4277 self.data.append((x, y))
4278 self.evalranges = 1
4280 def draw(self, graph):
4281 self.style.drawpoints(graph, self.data)
4284 class paramfunction:
4286 defaultstyle = line
4288 def __init__(self, varname, min, max, expression, title=helper.nodefault, points=100, parser=mathtree.parser(), context={}):
4289 if title is helper.nodefault:
4290 self.title = expression
4291 else:
4292 self.title = title
4293 self.varname = varname
4294 self.min = min
4295 self.max = max
4296 self.points = points
4297 self.expression = {}
4298 self.mathtrees = {}
4299 varlist, expressionlist = expression.split("=")
4300 parsestr = mathtree.ParseStr(expressionlist)
4301 for key in varlist.split(","):
4302 key = key.strip()
4303 if self.mathtrees.has_key(key):
4304 raise ValueError("multiple assignment in tuple")
4305 try:
4306 self.mathtrees[key] = parser.ParseMathTree(parsestr)
4307 break
4308 except mathtree.CommaFoundMathTreeParseError, e:
4309 self.mathtrees[key] = e.MathTree
4310 else:
4311 raise ValueError("unpack tuple of wrong size")
4312 if len(varlist.split(",")) != len(self.mathtrees.keys()):
4313 raise ValueError("unpack tuple of wrong size")
4314 self.data = []
4315 for i in range(self.points):
4316 context[self.varname] = self.min + (self.max-self.min)*i / (self.points-1.0)
4317 line = []
4318 for key, tree in self.mathtrees.items():
4319 line.append(tree.Calc(**context))
4320 self.data.append(line)
4322 def setstyle(self, graph, style):
4323 self.style = style
4324 columns = {}
4325 for key, index in zip(self.mathtrees.keys(), xrange(sys.maxint)):
4326 columns[key] = index
4327 self.style.setcolumns(graph, columns)
4329 def getranges(self):
4330 return self.style.getranges(self.data)
4332 def setranges(self, ranges):
4333 pass
4335 def draw(self, graph):
4336 self.style.drawpoints(graph, self.data)