ported splitaxis to new interfaces
[PyX/mjg.git] / pyx / graph.py
blobe3a534d6a8166fec018ba596dcac7f9a9cf1ff57
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 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"
58 __implements__ = _Imap
60 def setbasepoints(self, basepoints):
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 return self.y1 + self.dydx * (value - self.x1)
70 def invert(self, value):
71 return self.x1 + self.dxdy * (value - self.y1)
74 class _logmap:
75 "logarithmic mapping"
76 __implements__ = _Imap
78 def setbasepoints(self, basepoints):
79 self.dydx = ((basepoints[1][1] - basepoints[0][1]) /
80 float(math.log(basepoints[1][0]) - math.log(basepoints[0][0])))
81 self.dxdy = ((math.log(basepoints[1][0]) - math.log(basepoints[0][0])) /
82 float(basepoints[1][1] - basepoints[0][1]))
83 self.x1 = math.log(basepoints[0][0])
84 self.y1 = basepoints[0][1]
85 return self
87 def convert(self, value):
88 return self.y1 + self.dydx * (math.log(value) - self.x1)
90 def invert(self, value):
91 return math.exp(self.x1 + self.dxdy * (value - self.y1))
95 ################################################################################
96 # partitioner
97 # please note the nomenclature:
98 # - a partition is a list of tick instances; to reduce name clashes, a
99 # partition is called ticks
100 # - a partitioner is a class creating a single or several ticks
101 # - a axis has a part attribute where it stores a partitioner or/and some
102 # (manually set) ticks -> the part attribute is used to create the ticks
103 # in the axis finish method
104 ################################################################################
107 class frac:
108 """fraction class for rational arithmetics
109 the axis partitioning uses rational arithmetics (with infinite accuracy)
110 basically it contains self.enum and self.denom"""
112 def stringfrac(self, s):
113 "converts a string 0.123 into a frac"
114 expparts = s.split("e")
115 if len(expparts) > 2:
116 raise ValueError("multiple 'e' found in '%s'" % s)
117 commaparts = expparts[0].split(".")
118 if len(commaparts) > 2:
119 raise ValueError("multiple '.' found in '%s'" % expparts[0])
120 if len(commaparts) == 1:
121 commaparts = [commaparts[0], ""]
122 result = frac((1, 10l), power=len(commaparts[1]))
123 neg = len(commaparts[0]) and commaparts[0][0] == "-"
124 if neg:
125 commaparts[0] = commaparts[0][1:]
126 elif len(commaparts[0]) and commaparts[0][0] == "+":
127 commaparts[0] = commaparts[0][1:]
128 if len(commaparts[0]):
129 if not commaparts[0].isdigit():
130 raise ValueError("unrecognized characters in '%s'" % s)
131 x = long(commaparts[0])
132 else:
133 x = 0
134 if len(commaparts[1]):
135 if not commaparts[1].isdigit():
136 raise ValueError("unrecognized characters in '%s'" % s)
137 y = long(commaparts[1])
138 else:
139 y = 0
140 result.enum = x*result.denom+y
141 if neg:
142 result.enum = -result.enum
143 if len(expparts) == 2:
144 neg = expparts[1][0] == "-"
145 if neg:
146 expparts[1] = expparts[1][1:]
147 elif expparts[1][0] == "+":
148 expparts[1] = expparts[1][1:]
149 if not expparts[1].isdigit():
150 raise ValueError("unrecognized characters in '%s'" % s)
151 if neg:
152 result *= frac((1, 10l), power=long(expparts[1]))
153 else:
154 result *= frac((10, 1l), power=long(expparts[1]))
155 return result
157 def floatfrac(self, x, floatprecision):
158 "converts a float into a frac with final resolution"
159 if helper.isinteger(floatprecision) and floatprecision < 0:
160 # this would be extremly vulnerable
161 raise RuntimeError("float resolution must be non-negative integer")
162 return self.stringfrac(("%%.%ig" % floatprecision) % x)
164 def __init__(self, x, power=None, floatprecision=10):
165 "for power!=None: frac=(enum/denom)**power"
166 if helper.isnumber(x):
167 value = self.floatfrac(x, floatprecision)
168 enum, denom = value.enum, value.denom
169 elif helper.isstring(x):
170 fraction = x.split("/")
171 if len(fraction) > 2:
172 raise ValueError("multiple '/' found in '%s'" % x)
173 value = self.stringfrac(fraction[0])
174 if len(fraction) == 2:
175 value2 = self.stringfrac(fraction[1])
176 value = value / value2
177 enum, denom = value.enum, value.denom
178 else:
179 try:
180 enum, denom = x
181 except TypeError:
182 enum, denom = x.enum, x.denom
183 if not helper.isinteger(enum) or not helper.isinteger(denom): raise TypeError("integer type expected")
184 if not denom: raise ZeroDivisionError("zero denominator")
185 if power != None:
186 if not helper.isinteger(power): raise TypeError("integer type expected")
187 if power >= 0:
188 self.enum = long(enum) ** power
189 self.denom = long(denom) ** power
190 else:
191 self.enum = long(denom) ** (-power)
192 self.denom = long(enum) ** (-power)
193 else:
194 self.enum = enum
195 self.denom = denom
197 def __cmp__(self, other):
198 if other is None:
199 return 1
200 return cmp(self.enum * other.denom, other.enum * self.denom)
202 def __abs__(self):
203 return frac((abs(self.enum), abs(self.denom)))
205 def __mul__(self, other):
206 return frac((self.enum * other.enum, self.denom * other.denom))
208 def __div__(self, other):
209 return frac((self.enum * other.denom, self.denom * other.enum))
211 def __float__(self):
212 "caution: avoid final precision of floats"
213 return float(self.enum) / self.denom
215 def __str__(self):
216 return "%i/%i" % (self.enum, self.denom)
219 class tick(frac):
220 """tick class
221 a tick is a frac enhanced by
222 - self.ticklevel (0 = tick, 1 = subtick, etc.)
223 - self.labellevel (0 = label, 1 = sublabel, etc.)
224 - self.label (a string) and self.labelattrs (a list, defaults to [])
225 When ticklevel or labellevel is None, no tick or label is present at that value.
226 When label is None, it should be automatically created (and stored), once the
227 an axis painter needs it. Classes, which implement _Itexter do precisely that."""
229 def __init__(self, pos, ticklevel=0, labellevel=0, label=None, labelattrs=[], **kwargs):
230 """initializes the instance
231 - see class description for the parameter description
232 - **kwargs are passed to the frac constructor"""
233 frac.__init__(self, pos, **kwargs)
234 self.ticklevel = ticklevel
235 self.labellevel = labellevel
236 self.label = label
237 self.labelattrs = helper.ensurelist(labelattrs)[:]
239 def merge(self, other):
240 """merges two ticks together:
241 - the lower ticklevel/labellevel wins
242 - the label is *never* taken over from other
243 - the ticks should be at the same position (otherwise it doesn't make sense)
244 -> this is NOT checked"""
245 if self.ticklevel is None or (other.ticklevel is not None and other.ticklevel < self.ticklevel):
246 self.ticklevel = other.ticklevel
247 if self.labellevel is None or (other.labellevel is not None and other.labellevel < self.labellevel):
248 self.labellevel = other.labellevel
250 def __repr__(self):
251 return "tick(%r, %r, %s, %s, %s)" % (self.enum, self.denom, self.ticklevel, self.labellevel, self.label)
254 def _mergeticklists(list1, list2):
255 """helper function to merge tick lists
256 - return a merged list of ticks out of list1 and list2
257 - CAUTION: original lists have to be ordered
258 (the returned list is also ordered)
259 - CAUTION: original lists are modified and they share references to
260 the result list!"""
261 # TODO: improve this using bisect?!
262 if list1 is None: return list2
263 if list2 is None: return list1
264 i = 0
265 j = 0
266 try:
267 while 1: # we keep on going until we reach an index error
268 while list2[j] < list1[i]: # insert tick
269 list1.insert(i, list2[j])
270 i += 1
271 j += 1
272 if list2[j] == list1[i]: # merge tick
273 list1[i].merge(list2[j])
274 j += 1
275 i += 1
276 except IndexError:
277 if j < len(list2):
278 list1 += list2[j:]
279 return list1
282 def _mergelabels(ticks, labels):
283 """helper function to merge labels into ticks
284 - when labels is not None, the label of all ticks with
285 labellevel different from None are set
286 - labels need to be a sequence of sequences of strings,
287 where the first sequence contain the strings to be
288 used as labels for the ticks with labellevel 0,
289 the second sequence for labellevel 1, etc.
290 - when the maximum labellevel is 0, just a sequence of
291 strings might be provided as the labels argument
292 - IndexError is raised, when a sequence length doesn't match"""
293 if helper.issequenceofsequences(labels):
294 for label, level in zip(labels, xrange(sys.maxint)):
295 usetext = helper.ensuresequence(label)
296 i = 0
297 for tick in ticks:
298 if tick.labellevel == level:
299 tick.label = usetext[i]
300 i += 1
301 if i != len(usetext):
302 raise IndexError("wrong sequence length of labels at level %i" % level)
303 elif labels is not None:
304 usetext = helper.ensuresequence(labels)
305 i = 0
306 for tick in ticks:
307 if tick.labellevel == 0:
308 tick.label = usetext[i]
309 i += 1
310 if i != len(usetext):
311 raise IndexError("wrong sequence length of labels")
314 class _Ipart:
315 """interface definition of a partition scheme
316 partition schemes are used to create a list of ticks"""
318 def defaultpart(self, min, max, extendmin, extendmax):
319 """create a partition
320 - returns an ordered list of ticks for the interval min to max
321 - the interval is given in float numbers, thus an appropriate
322 conversion to rational numbers has to be performed
323 - extendmin and extendmax are booleans (integers)
324 - when extendmin or extendmax is set, the ticks might
325 extend the min-max range towards lower and higher
326 ranges, respectively"""
328 def lesspart(self):
329 """create another partition which contains less ticks
330 - this method is called several times after a call of defaultpart
331 - returns an ordered list of ticks with less ticks compared to
332 the partition returned by defaultpart and by previous calls
333 of lesspart
334 - the creation of a partition with strictly *less* ticks
335 is not to be taken serious
336 - the method might return None, when no other appropriate
337 partition can be created"""
340 def morepart(self):
341 """create another partition which contains more ticks
342 see lesspart, but increase the number of ticks"""
345 class manualpart:
346 """manual partition scheme
347 ticks and labels at positions explicitly provided to the constructor"""
349 __implements__ = _Ipart
351 def __init__(self, tickpos=None, labelpos=None, labels=None, mix=()):
352 """configuration of the partition scheme
353 - tickpos and labelpos should be a sequence of sequences, where
354 the first sequence contains the values to be used for
355 ticks with ticklevel/labellevel 0, the second sequence for
356 ticklevel/labellevel 1, etc.
357 - tickpos and labelpos values are passed to the frac constructor
358 - when the maximum ticklevel/labellevel is 0, just a sequence
359 might be provided in tickpos and labelpos
360 - when labelpos is None and tickpos is not None, the tick entries
361 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
362 - labels are applied to the resulting partition via the
363 mergelabels function (additional information available there)
364 - mix specifies another partition to be merged into the
365 created partition"""
366 if tickpos is None and labelpos is not None:
367 self.tickpos = helper.ensuresequence(helper.getsequenceno(labelpos, 0))
368 else:
369 self.tickpos = tickpos
370 if labelpos is None and tickpos is not None:
371 self.labelpos = helper.ensuresequence(helper.getsequenceno(tickpos, 0))
372 else:
373 self.labelpos = labelpos
374 self.labels = labels
375 self.mix = mix
377 def checkfraclist(self, *fracs):
378 "orders a list of fracs, equal entries are not allowed"
379 if not len(fracs): return ()
380 sorted = list(fracs)
381 sorted.sort()
382 last = sorted[0]
383 for item in sorted[1:]:
384 if last == item:
385 raise ValueError("duplicate entry found")
386 last = item
387 return sorted
389 def part(self):
390 "create the partition as described in the constructor"
391 ticks = list(self.mix)
392 if helper.issequenceofsequences(self.tickpos):
393 for fracs, level in zip(self.tickpos, xrange(sys.maxint)):
394 ticks = _mergeticklists(ticks, [tick((f.enum, f.denom), ticklevel=level, labellevel=None)
395 for f in self.checkfraclist(*map(frac, helper.ensuresequence(fracs)))])
396 else:
397 map(frac, helper.ensuresequence(self.tickpos))
398 ticks = _mergeticklists(ticks, [tick((f.enum, f.denom), ticklevel=0, labellevel=None)
399 for f in self.checkfraclist(*map(frac, helper.ensuresequence(self.tickpos)))])
401 if helper.issequenceofsequences(self.labelpos):
402 for fracs, level in zip(self.labelpos, xrange(sys.maxint)):
403 ticks = _mergeticklists(ticks, [tick((f.enum, f.denom), ticklevel=None, labellevel = level)
404 for f in self.checkfraclist(*map(frac, helper.ensuresequence(fracs)))])
405 else:
406 ticks = _mergeticklists(ticks, [tick((f.enum, f.denom), ticklevel=None, labellevel = 0)
407 for f in self.checkfraclist(*map(frac, helper.ensuresequence(self.labelpos)))])
409 _mergelabels(ticks, self.labels)
411 return ticks
413 def defaultpart(self, min, max, extendmin, extendmax):
414 # XXX: we do not take care of the parameters -> correct?
415 return self.part()
417 def lesspart(self):
418 return None
420 def morepart(self):
421 return None
424 class linpart:
425 """linear partition scheme
426 ticks and label distances are explicitly provided to the constructor"""
428 __implements__ = _Ipart
430 def __init__(self, tickdist=None, labeldist=None, labels=None, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
431 """configuration of the partition scheme
432 - tickdist and labeldist should be a sequence, where the first value
433 is the distance between ticks with ticklevel/labellevel 0,
434 the second sequence for ticklevel/labellevel 1, etc.;
435 a single entry is allowed without being a sequence
436 - tickdist and labeldist values are passed to the frac constructor
437 - when labeldist is None and tickdist is not None, the tick entries
438 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
439 - labels are applied to the resulting partition via the
440 mergelabels function (additional information available there)
441 - extendtick allows for the extension of the range given to the
442 defaultpart method to include the next tick with the specified
443 level (None turns off this feature); note, that this feature is
444 also disabled, when an axis prohibits its range extension by
445 the extendmin/extendmax variables given to the defaultpart method
446 - extendlabel is analogous to extendtick, but for labels
447 - epsilon allows for exceeding the axis range by this relative
448 value (relative to the axis range given to the defaultpart method)
449 without creating another tick specified by extendtick/extendlabel
450 - mix specifies another partition to be merged into the
451 created partition"""
452 if tickdist is None and labeldist is not None:
453 self.ticklist = (frac(helper.ensuresequence(labeldist)[0]),)
454 else:
455 self.ticklist = map(frac, helper.ensuresequence(tickdist))
456 if labeldist is None and tickdist is not None:
457 self.labellist = (frac(helper.ensuresequence(tickdist)[0]),)
458 else:
459 self.labellist = map(frac, helper.ensuresequence(labeldist))
460 self.labels = labels
461 self.extendtick = extendtick
462 self.extendlabel = extendlabel
463 self.epsilon = epsilon
464 self.mix = mix
466 def extendminmax(self, min, max, frac, extendmin, extendmax):
467 """return new min, max tuple extending the range min, max
468 - frac is the tick distance to be used
469 - extendmin and extendmax are booleans to allow for the extension"""
470 if extendmin:
471 min = float(frac) * math.floor(min / float(frac) + self.epsilon)
472 if extendmax:
473 max = float(frac) * math.ceil(max / float(frac) - self.epsilon)
474 return min, max
476 def getticks(self, min, max, frac, ticklevel=None, labellevel=None):
477 """return a list of equal spaced ticks
478 - the tick distance is frac, the ticklevel is set to ticklevel and
479 the labellevel is set to labellevel
480 - min, max is the range where ticks should be placed"""
481 imin = int(math.ceil(min / float(frac) - 0.5 * self.epsilon))
482 imax = int(math.floor(max / float(frac) + 0.5 * self.epsilon))
483 ticks = []
484 for i in range(imin, imax + 1):
485 ticks.append(tick((long(i) * frac.enum, frac.denom), ticklevel=ticklevel, labellevel=labellevel))
486 return ticks
488 def defaultpart(self, min, max, extendmin, extendmax):
489 if self.extendtick is not None and len(self.ticklist) > self.extendtick:
490 min, max = self.extendminmax(min, max, self.ticklist[self.extendtick], extendmin, extendmax)
491 if self.extendlabel is not None and len(self.labellist) > self.extendlabel:
492 min, max = self.extendminmax(min, max, self.labellist[self.extendlabel], extendmin, extendmax)
494 ticks = list(self.mix)
495 for i in range(len(self.ticklist)):
496 ticks = _mergeticklists(ticks, self.getticks(min, max, self.ticklist[i], ticklevel = i))
497 for i in range(len(self.labellist)):
498 ticks = _mergeticklists(ticks, self.getticks(min, max, self.labellist[i], labellevel = i))
500 _mergelabels(ticks, self.labels)
502 return ticks
504 def lesspart(self):
505 return None
507 def morepart(self):
508 return None
511 class autolinpart:
512 """automatic linear partition scheme
513 - possible tick distances are explicitly provided to the constructor
514 - tick distances are adjusted to the axis range by multiplication or division by 10"""
516 __implements__ = _Ipart
518 defaultvariants = ((frac((1, 1)), frac((1, 2))),
519 (frac((2, 1)), frac((1, 1))),
520 (frac((5, 2)), frac((5, 4))),
521 (frac((5, 1)), frac((5, 2))))
523 def __init__(self, variants=defaultvariants, extendtick=0, epsilon=1e-10, mix=()):
524 """configuration of the partition scheme
525 - variants is a sequence of tickdist
526 - tickdist should be a sequence, where the first value
527 is the distance between ticks with ticklevel 0,
528 the second for ticklevel 1, etc.
529 - tickdist values are passed to the frac constructor
530 - labellevel is set to None except for those ticks in the partitions,
531 where ticklevel is zero. There labellevel is also set to zero.
532 - extendtick allows for the extension of the range given to the
533 defaultpart method to include the next tick with the specified
534 level (None turns off this feature); note, that this feature is
535 also disabled, when an axis prohibits its range extension by
536 the extendmin/extendmax variables given to the defaultpart method
537 - epsilon allows for exceeding the axis range by this relative
538 value (relative to the axis range given to the defaultpart method)
539 without creating another tick specified by extendtick
540 - mix specifies another partition to be merged into the
541 created partition"""
542 self.variants = variants
543 self.extendtick = extendtick
544 self.epsilon = epsilon
545 self.mix = mix
547 def defaultpart(self, min, max, extendmin, extendmax):
548 logmm = math.log(max - min) / math.log(10)
549 if logmm < 0: # correction for rounding towards zero of the int routine
550 base = frac((10L, 1), int(logmm - 1))
551 else:
552 base = frac((10L, 1), int(logmm))
553 ticks = map(frac, self.variants[0])
554 useticks = [tick * base for tick in ticks]
555 self.lesstickindex = self.moretickindex = 0
556 self.lessbase = frac((base.enum, base.denom))
557 self.morebase = frac((base.enum, base.denom))
558 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
559 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
560 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
562 def lesspart(self):
563 if self.lesstickindex < len(self.variants) - 1:
564 self.lesstickindex += 1
565 else:
566 self.lesstickindex = 0
567 self.lessbase.enum *= 10
568 ticks = map(frac, self.variants[self.lesstickindex])
569 useticks = [tick * self.lessbase for tick in ticks]
570 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
571 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
573 def morepart(self):
574 if self.moretickindex:
575 self.moretickindex -= 1
576 else:
577 self.moretickindex = len(self.variants) - 1
578 self.morebase.denom *= 10
579 ticks = map(frac, self.variants[self.moretickindex])
580 useticks = [tick * self.morebase for tick in ticks]
581 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
582 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
585 class preexp:
586 """storage class for the definition of logarithmic axes partitions
587 instances of this class define tick positions suitable for
588 logarithmic axes by the following instance variables:
589 - exp: integer, which defines multiplicator (usually 10)
590 - pres: sequence of tick positions (rational numbers, e.g. instances of frac)
591 possible positions are these tick positions and arbitrary divisions
592 and multiplications by the exp value"""
594 def __init__(self, pres, exp):
595 "create a preexp instance and store its pres and exp information"
596 self.pres = helper.ensuresequence(pres)
597 self.exp = exp
600 class logpart(linpart):
601 """logarithmic partition scheme
602 ticks and label positions are explicitly provided to the constructor"""
604 __implements__ = _Ipart
606 pre1exp5 = preexp(frac((1, 1)), 100000)
607 pre1exp4 = preexp(frac((1, 1)), 10000)
608 pre1exp3 = preexp(frac((1, 1)), 1000)
609 pre1exp2 = preexp(frac((1, 1)), 100)
610 pre1exp = preexp(frac((1, 1)), 10)
611 pre125exp = preexp((frac((1, 1)), frac((2, 1)), frac((5, 1))), 10)
612 pre1to9exp = preexp(map(lambda x: frac((x, 1)), range(1, 10)), 10)
613 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
615 def __init__(self, tickpos=None, labelpos=None, labels=None, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
616 """configuration of the partition scheme
617 - tickpos and labelpos should be a sequence, where the first entry
618 is a preexp instance describing ticks with ticklevel/labellevel 0,
619 the second is a preexp instance for ticklevel/labellevel 1, etc.;
620 a single entry is allowed without being a sequence
621 - when labelpos is None and tickpos is not None, the tick entries
622 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
623 - labels are applied to the resulting partition via the
624 mergetexts function (additional information available there)
625 - extendtick allows for the extension of the range given to the
626 defaultpart method to include the next tick with the specified
627 level (None turns off this feature); note, that this feature is
628 also disabled, when an axis prohibits its range extension by
629 the extendmin/extendmax variables given to the defaultpart method
630 - extendlabel is analogous to extendtick, but for labels
631 - epsilon allows for exceeding the axis range by this relative
632 logarithm value (relative to the logarithm axis range given
633 to the defaultpart method) without creating another tick
634 specified by extendtick/extendlabel
635 - mix specifies another partition to be merged into the
636 created partition"""
637 if tickpos is None and labels is not None:
638 self.ticklist = (helper.ensuresequence(labelpos)[0],)
639 else:
640 self.ticklist = helper.ensuresequence(tickpos)
642 if labelpos is None and tickpos is not None:
643 self.labellist = (helper.ensuresequence(tickpos)[0],)
644 else:
645 self.labellist = helper.ensuresequence(labelpos)
646 self.labels = labels
647 self.extendtick = extendtick
648 self.extendlabel = extendlabel
649 self.epsilon = epsilon
650 self.mix = mix
652 def extendminmax(self, min, max, preexp, extendmin, extendmax):
653 """return new min, max tuple extending the range min, max
654 preexp describes the allowed tick positions
655 extendmin and extendmax are booleans to allow for the extension"""
656 minpower = None
657 maxpower = None
658 for i in xrange(len(preexp.pres)):
659 imin = int(math.floor(math.log(min / float(preexp.pres[i])) /
660 math.log(preexp.exp) + self.epsilon)) + 1
661 imax = int(math.ceil(math.log(max / float(preexp.pres[i])) /
662 math.log(preexp.exp) - self.epsilon)) - 1
663 if minpower is None or imin < minpower:
664 minpower, minindex = imin, i
665 if maxpower is None or imax >= maxpower:
666 maxpower, maxindex = imax, i
667 if minindex:
668 minfrac = preexp.pres[minindex - 1]
669 else:
670 minfrac = preexp.pres[-1]
671 minpower -= 1
672 if maxindex != len(preexp.pres) - 1:
673 maxfrac = preexp.pres[maxindex + 1]
674 else:
675 maxfrac = preexp.pres[0]
676 maxpower += 1
677 if extendmin:
678 min = float(minfrac) * float(preexp.exp) ** minpower
679 if extendmax:
680 max = float(maxfrac) * float(preexp.exp) ** maxpower
681 return min, max
683 def getticks(self, min, max, preexp, ticklevel=None, labellevel=None):
684 """return a list of ticks
685 - preexp describes the allowed tick positions
686 - the ticklevel of the ticks is set to ticklevel and
687 the labellevel is set to labellevel
688 - min, max is the range where ticks should be placed"""
689 ticks = list(self.mix)
690 minimin = 0
691 maximax = 0
692 for f in preexp.pres:
693 fracticks = []
694 imin = int(math.ceil(math.log(min / float(f)) /
695 math.log(preexp.exp) - 0.5 * self.epsilon))
696 imax = int(math.floor(math.log(max / float(f)) /
697 math.log(preexp.exp) + 0.5 * self.epsilon))
698 for i in range(imin, imax + 1):
699 pos = f * frac((preexp.exp, 1), i)
700 fracticks.append(tick((pos.enum, pos.denom), ticklevel = ticklevel, labellevel = labellevel))
701 ticks = _mergeticklists(ticks, fracticks)
702 return ticks
705 class autologpart(logpart):
706 """automatic logarithmic partition scheme
707 possible tick positions are explicitly provided to the constructor"""
709 __implements__ = _Ipart
711 defaultvariants = (((logpart.pre1exp, # ticks
712 logpart.pre1to9exp), # subticks
713 (logpart.pre1exp, # labels
714 logpart.pre125exp)), # sublevels
716 ((logpart.pre1exp, # ticks
717 logpart.pre1to9exp), # subticks
718 None), # labels like ticks
720 ((logpart.pre1exp2, # ticks
721 logpart.pre1exp), # subticks
722 None), # labels like ticks
724 ((logpart.pre1exp3, # ticks
725 logpart.pre1exp), # subticks
726 None), # labels like ticks
728 ((logpart.pre1exp4, # ticks
729 logpart.pre1exp), # subticks
730 None), # labels like ticks
732 ((logpart.pre1exp5, # ticks
733 logpart.pre1exp), # subticks
734 None)) # labels like ticks
736 def __init__(self, variants=defaultvariants, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
737 """configuration of the partition scheme
738 - variants should be a sequence of pairs of sequences of preexp
739 instances
740 - within each pair the first sequence contains preexp, where
741 the first preexp instance describes ticks positions with
742 ticklevel 0, the second preexp for ticklevel 1, etc.
743 - the second sequence within each pair describes the same as
744 before, but for labels
745 - within each pair: when the second entry (for the labels) is None
746 and the first entry (for the ticks) ticks is not None, the tick
747 entries for ticklevel 0 are used for labels and vice versa
748 (ticks<->labels)
749 - extendtick allows for the extension of the range given to the
750 defaultpart method to include the next tick with the specified
751 level (None turns off this feature); note, that this feature is
752 also disabled, when an axis prohibits its range extension by
753 the extendmin/extendmax variables given to the defaultpart method
754 - extendlabel is analogous to extendtick, but for labels
755 - epsilon allows for exceeding the axis range by this relative
756 logarithm value (relative to the logarithm axis range given
757 to the defaultpart method) without creating another tick
758 specified by extendtick/extendlabel
759 - mix specifies another partition to be merged into the
760 created partition"""
761 self.variants = variants
762 if len(variants) > 2:
763 self.variantsindex = divmod(len(variants), 2)[0]
764 else:
765 self.variantsindex = 0
766 self.extendtick = extendtick
767 self.extendlabel = extendlabel
768 self.epsilon = epsilon
769 self.mix = mix
771 def defaultpart(self, min, max, extendmin, extendmax):
772 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
773 self.morevariantsindex = self.variantsindex
774 self.lessvariantsindex = self.variantsindex
775 part = logpart(tickpos=self.variants[self.variantsindex][0], labelpos=self.variants[self.variantsindex][1],
776 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
777 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
779 def lesspart(self):
780 self.lessvariantsindex += 1
781 if self.lessvariantsindex < len(self.variants):
782 part = logpart(tickpos=self.variants[self.lessvariantsindex][0], labelpos=self.variants[self.lessvariantsindex][1],
783 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
784 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
786 def morepart(self):
787 self.morevariantsindex -= 1
788 if self.morevariantsindex >= 0:
789 part = logpart(tickpos=self.variants[self.morevariantsindex][0], labelpos=self.variants[self.morevariantsindex][1],
790 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
791 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
795 ################################################################################
796 # rater
797 # conseptional remarks:
798 # - raters are used to calculate a rating for a realization of something
799 # - here, a rating means a positive floating point value
800 # - ratings are used to order those realizations by their suitability (lower
801 # ratings are better)
802 # - a rating of None means not suitable at all (those realizations should be
803 # thrown out)
804 ################################################################################
807 class cuberater:
808 """a value rater
809 - a cube rater has an optimal value, where the rate becomes zero
810 - for a left (below the optimum) and a right value (above the optimum),
811 the rating is value is set to 1 (modified by an overall weight factor
812 for the rating)
813 - the analytic form of the rating is cubic for both, the left and
814 the right side of the rater, independently"""
816 # __implements__ = sole implementation
818 def __init__(self, opt, left=None, right=None, weight=1):
819 """initializes the rater
820 - by default, left is set to zero, right is set to 3*opt
821 - left should be smaller than opt, right should be bigger than opt
822 - weight should be positive and is a factor multiplicated to the rates"""
823 if left is None:
824 left = 0
825 if right is None:
826 right = 3*opt
827 self.opt = opt
828 self.left = left
829 self.right = right
830 self.weight = weight
832 def rate(self, value, density):
833 """returns a rating for a value
834 - the density lineary rescales the rater (the optimum etc.),
835 e.g. a value bigger than one increases the optimum (when it is
836 positive) and a value lower than one decreases the optimum (when
837 it is positive); the density itself should be positive"""
838 opt = self.opt * density
839 if value < opt:
840 other = self.left * density
841 elif value > opt:
842 other = self.right * density
843 else:
844 return 0
845 factor = (value - opt) / float(other - opt)
846 return self.weight * (factor ** 3)
849 class distancerater:
850 # TODO: update docstring
851 """a distance rater (rates a list of distances)
852 - the distance rater rates a list of distances by rating each independently
853 and returning the average rate
854 - there is an optimal value, where the rate becomes zero
855 - the analytic form is linary for values above the optimal value
856 (twice the optimal value has the rating one, three times the optimal
857 value has the rating two, etc.)
858 - the analytic form is reciprocal subtracting one for values below the
859 optimal value (halve the optimal value has the rating one, one third of
860 the optimal value has the rating two, etc.)"""
862 # __implements__ = sole implementation
864 def __init__(self, opt, weight=0.1):
865 """inititializes the rater
866 - opt is the optimal length (a visual PyX length)
867 - weight should be positive and is a factor multiplicated to the rates"""
868 self.opt_str = opt
869 self.weight = weight
871 def rate(self, distances, density):
872 """rate distances
873 - the distances are a sequence of positive floats in PostScript points
874 - the density lineary rescales the rater (the optimum etc.),
875 e.g. a value bigger than one increases the optimum (when it is
876 positive) and a value lower than one decreases the optimum (when
877 it is positive); the density itself should be positive"""
878 if len(distances):
879 opt = unit.topt(unit.length(self.opt_str, default_type="v")) / density
880 rate = 0
881 for distance in distances:
882 if distance < opt:
883 rate += self.weight * (opt / distance - 1)
884 else:
885 rate += self.weight * (distance / opt - 1)
886 return rate / float(len(distances))
889 class axisrater:
890 """a rater for ticks
891 - the rating of axes is splited into two separate parts:
892 - rating of the ticks in terms of the number of ticks, subticks,
893 labels, etc.
894 - rating of the label distances
895 - in the end, a rate for ticks is the sum of these rates
896 - it is useful to first just rate the number of ticks etc.
897 and selecting those partitions, where this fits well -> as soon
898 as an complete rate (the sum of both parts from the list above)
899 of a first ticks is below a rate of just the number of ticks,
900 subticks labels etc. of other ticks, those other ticks will never
901 be better than the first one -> we gain speed by minimizing the
902 number of ticks, where label distances have to be taken into account)
903 - both parts of the rating are shifted into instances of raters
904 defined above --- right now, there is not yet a strict interface
905 for this delegation (should be done as soon as it is needed)"""
907 # __implements__ = sole implementation
909 linticks = (cuberater(4), cuberater(10, weight=0.5), )
910 linlabels = (cuberater(4), )
911 logticks = (cuberater(5, right=20), cuberater(20, right=100, weight=0.5), )
912 loglabels = (cuberater(5, right=20), cuberater(5, left=-20, right=20, weight=0.5), )
913 stdtickrange = cuberater(1, weight=2)
914 stddistance = distancerater("1 cm")
916 def __init__(self, ticks=linticks, labels=linlabels, tickrange=stdtickrange, distance=stddistance):
917 """initializes the axis rater
918 - ticks and labels are lists of instances of a value rater
919 - the first entry in ticks rate the number of ticks, the
920 second the number of subticks, etc.; when there are no
921 ticks of a level or there is not rater for a level, the
922 level is just ignored
923 - labels is analogous, but for labels
924 - within the rating, all ticks with a higher level are
925 considered as ticks for a given level
926 - tickrange is a value rater instance, which rates the covering
927 of an axis range by the ticks (as a relative value of the
928 tick range vs. the axis range), ticks might cover less or
929 more than the axis range (for the standard automatic axis
930 partition schemes an extention of the axis range is normal
931 and should get some penalty)
932 - distance is an distance rater instance"""
933 self.rateticks = ticks
934 self.ratelabels = labels
935 self.tickrange = tickrange
936 self.distance = distance
938 def ratepart(self, axis, ticks, density):
939 """rates ticks by the number of ticks, subticks, labels etc.
940 - takes into account the number of ticks, subticks, labels
941 etc. and the coverage of the axis range by the ticks
942 - when there are no ticks of a level or there was not rater
943 given in the constructor for a level, the level is just
944 ignored
945 - the method returns the sum of the rating results divided
946 by the sum of the weights of the raters
947 - within the rating, all ticks with a higher level are
948 considered as ticks for a given level"""
949 maxticklevel = maxlabellevel = 0
950 for tick in ticks:
951 if tick.ticklevel >= maxticklevel:
952 maxticklevel = tick.ticklevel + 1
953 if tick.labellevel >= maxlabellevel:
954 maxlabellevel = tick.labellevel + 1
955 numticks = [0]*maxticklevel
956 numlabels = [0]*maxlabellevel
957 for tick in ticks:
958 if tick.ticklevel is not None:
959 for level in range(tick.ticklevel, maxticklevel):
960 numticks[level] += 1
961 if tick.labellevel is not None:
962 for level in range(tick.labellevel, maxlabellevel):
963 numlabels[level] += 1
964 rate = 0
965 weight = 0
966 for numtick, rater in zip(numticks, self.rateticks):
967 rate += rater.rate(numtick, density)
968 weight += rater.weight
969 for numlabel, rater in zip(numlabels, self.ratelabels):
970 rate += rater.rate(numlabel, density)
971 weight += rater.weight
972 if len(ticks):
973 # XXX: tickrange was not yet applied
974 rate += self.tickrange.rate(axis.convert(float(ticks[-1]) * axis.divisor) -
975 axis.convert(float(ticks[0]) * axis.divisor), 1)
976 else:
977 rate += self.tickrange.rate(0)
978 weight += self.tickrange.weight
979 return rate/weight
981 def ratelayout(self, axiscanvas, density):
982 """rate distances of the labels in an axis canvas
983 - the distances should be collected as box distances of
984 subsequent labels
985 - the axiscanvas provides a labels attribute for easy
986 access to the labels whose distances have to be taken
987 into account
988 - the density is used within the distancerate instance"""
989 if len(axiscanvas.labels) > 1:
990 try:
991 distances = [axiscanvas.labels[i]._boxdistance(axiscanvas.labels[i+1]) for i in range(len(axiscanvas.labels) - 1)]
992 except box.BoxCrossError:
993 return None
994 return self.distance.rate(distances, density)
995 else:
996 return 0
999 ################################################################################
1000 # texter
1001 # texter automatically create labels for tick instances
1002 ################################################################################
1005 class _Itexter:
1007 def labels(self, ticks):
1008 """fill the label attribute of ticks
1009 - ticks is a list of instances of tick
1010 - for each element of ticks the value of the attribute label is set to
1011 a string appropriate to the attributes enum and denom of that tick
1012 instance
1013 - label attributes of the tick instances are just kept, whenever they
1014 are not equal to None
1015 - the method might extend the labelattrs attribute of the ticks"""
1018 class rationaltexter:
1019 "a texter creating rational labels (e.g. 'a/b' or even 'a \over b')"
1020 # XXX: we use divmod here to be more expicit
1022 __implements__ = _Itexter
1024 def __init__(self, prefix="", infix="", suffix="",
1025 enumprefix="", enuminfix="", enumsuffix="",
1026 denomprefix="", denominfix="", denomsuffix="",
1027 minus="-", minuspos=0, over=r"{{%s}\over{%s}}",
1028 equaldenom=0, skip1=1, skipenum0=1, skipenum1=1, skipdenom1=1,
1029 labelattrs=textmodule.mathmode):
1030 r"""initializes the instance
1031 - prefix, infix, and suffix (strings) are added at the begin,
1032 immediately after the minus, and at the end of the label,
1033 respectively
1034 - prefixenum, infixenum, and suffixenum (strings) are added
1035 to the labels enumerator correspondingly
1036 - prefixdenom, infixdenom, and suffixdenom (strings) are added
1037 to the labels denominator correspondingly
1038 - minus (string) is inserted for negative numbers
1039 - minuspos is an integer, which determines the position, where the
1040 minus sign has to be placed; the following values are allowed:
1041 1 - writes the minus in front of the enumerator
1042 0 - writes the minus in front of the hole fraction
1043 -1 - writes the minus in front of the denominator
1044 - over (string) is taken as a format string generating the
1045 fraction bar; it has to contain exactly two string insert
1046 operators "%s" -- the first for the enumerator and the second
1047 for the denominator; by far the most common examples are
1048 r"{{%s}\over{%s}}" and "{{%s}/{%s}}"
1049 - usually the enumerator and denominator are canceled; however,
1050 when equaldenom is set, the least common multiple of all
1051 denominators is used
1052 - skip1 (boolean) just prints the prefix, the minus (if present),
1053 the infix and the suffix, when the value is plus or minus one
1054 and at least one of prefix, infix and the suffix is present
1055 - skipenum0 (boolean) just prints a zero instead of
1056 the hole fraction, when the enumerator is zero;
1057 no prefixes, infixes, and suffixes are taken into account
1058 - skipenum1 (boolean) just prints the enumprefix, the minus (if present),
1059 the enuminfix and the enumsuffix, when the enum value is plus or minus one
1060 and at least one of enumprefix, enuminfix and the enumsuffix is present
1061 - skipdenom1 (boolean) just prints the enumerator instead of
1062 the hole fraction, when the denominator is one and none of the parameters
1063 denomprefix, denominfix and denomsuffix are set and minuspos is not -1 or the
1064 fraction is positive
1065 - labelattrs is a sequence of attributes for a texrunners text method;
1066 a single is allowed without being a sequence; None is considered as
1067 an empty sequence"""
1068 self.prefix = prefix
1069 self.infix = infix
1070 self.suffix = suffix
1071 self.enumprefix = enumprefix
1072 self.enuminfix = enuminfix
1073 self.enumsuffix = enumsuffix
1074 self.denomprefix = denomprefix
1075 self.denominfix = denominfix
1076 self.denomsuffix = denomsuffix
1077 self.minus = minus
1078 self.minuspos = minuspos
1079 self.over = over
1080 self.equaldenom = equaldenom
1081 self.skip1 = skip1
1082 self.skipenum0 = skipenum0
1083 self.skipenum1 = skipenum1
1084 self.skipdenom1 = skipdenom1
1085 self.labelattrs = helper.ensurelist(labelattrs)
1087 def gcd(self, *n):
1088 """returns the greates common divisor of all elements in n
1089 - the elements of n must be non-negative integers
1090 - return None if the number of elements is zero
1091 - the greates common divisor is not affected when some
1092 of the elements are zero, but it becomes zero when
1093 all elements are zero"""
1094 if len(n) == 2:
1095 i, j = n
1096 if i < j:
1097 i, j = j, i
1098 while j > 0:
1099 i, (dummy, j) = j, divmod(i, j)
1100 return i
1101 if len(n):
1102 res = n[0]
1103 for i in n[1:]:
1104 res = self.gcd(res, i)
1105 return res
1107 def lcm(self, *n):
1108 """returns the least common multiple of all elements in n
1109 - the elements of n must be non-negative integers
1110 - return None if the number of elements is zero
1111 - the least common multiple is zero when some of the
1112 elements are zero"""
1113 if len(n):
1114 res = n[0]
1115 for i in n[1:]:
1116 res = divmod(res * i, self.gcd(res, i))[0]
1117 return res
1119 def labels(self, ticks):
1120 labeledticks = []
1121 for tick in ticks:
1122 if tick.label is None and tick.labellevel is not None:
1123 labeledticks.append(tick)
1124 tick.temp_fracenum = tick.enum
1125 tick.temp_fracdenom = tick.denom
1126 tick.temp_fracminus = 1
1127 if tick.temp_fracenum < 0:
1128 tick.temp_fracminus *= -1
1129 tick.temp_fracenum *= -1
1130 if tick.temp_fracdenom < 0:
1131 tick.temp_fracminus *= -1
1132 tick.temp_fracdenom *= -1
1133 gcd = self.gcd(tick.temp_fracenum, tick.temp_fracdenom)
1134 (tick.temp_fracenum, dummy1), (tick.temp_fracdenom, dummy2) = divmod(tick.temp_fracenum, gcd), divmod(tick.temp_fracdenom, gcd)
1135 if self.equaldenom:
1136 equaldenom = self.lcm(*[tick.temp_fracdenom for tick in ticks if tick.label is None])
1137 if equaldenom is not None:
1138 for tick in labeledticks:
1139 factor, dummy = divmod(equaldenom, tick.temp_fracdenom)
1140 tick.temp_fracenum, tick.temp_fracdenom = factor * tick.temp_fracenum, factor * tick.temp_fracdenom
1141 for tick in labeledticks:
1142 fracminus = fracenumminus = fracdenomminus = ""
1143 if tick.temp_fracminus == -1:
1144 if self.minuspos == 0:
1145 fracminus = self.minus
1146 elif self.minuspos == 1:
1147 fracenumminus = self.minus
1148 elif self.minuspos == -1:
1149 fracdenomminus = self.minus
1150 else:
1151 raise RuntimeError("invalid minuspos")
1152 if self.skipenum0 and tick.temp_fracenum == 0:
1153 tick.label = "0"
1154 elif (self.skip1 and self.skipdenom1 and tick.temp_fracenum == 1 and tick.temp_fracdenom == 1 and
1155 (len(self.prefix) or len(self.infix) or len(self.suffix)) and
1156 not len(fracenumminus) and not len(self.enumprefix) and not len(self.enuminfix) and not len(self.enumsuffix) and
1157 not len(fracdenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix)):
1158 tick.label = "%s%s%s%s" % (self.prefix, fracminus, self.infix, self.suffix)
1159 else:
1160 if self.skipenum1 and tick.temp_fracenum == 1 and (len(self.enumprefix) or len(self.enuminfix) or len(self.enumsuffix)):
1161 tick.temp_fracenum = "%s%s%s%s" % (self.enumprefix, fracenumminus, self.enuminfix, self.enumsuffix)
1162 else:
1163 tick.temp_fracenum = "%s%s%s%i%s" % (self.enumprefix, fracenumminus, self.enuminfix, tick.temp_fracenum, self.enumsuffix)
1164 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):
1165 frac = tick.temp_fracenum
1166 else:
1167 tick.temp_fracdenom = "%s%s%s%i%s" % (self.denomprefix, fracdenomminus, self.denominfix, tick.temp_fracdenom, self.denomsuffix)
1168 frac = self.over % (tick.temp_fracenum, tick.temp_fracdenom)
1169 tick.label = "%s%s%s%s%s" % (self.prefix, fracminus, self.infix, frac, self.suffix)
1170 tick.labelattrs.extend(self.labelattrs)
1172 # del tick.temp_fracenum # we've inserted those temporary variables ... and do not care any longer about them
1173 # del tick.temp_fracdenom
1174 # del tick.temp_fracminus
1178 class decimaltexter:
1179 "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')"
1181 __implements__ = _Itexter
1183 def __init__(self, prefix="", infix="", suffix="", equalprecision=0,
1184 decimalsep=".", thousandsep="", thousandthpartsep="",
1185 minus="-", period=r"\overline{%s}", labelattrs=textmodule.mathmode):
1186 r"""initializes the instance
1187 - prefix, infix, and suffix (strings) are added at the begin,
1188 immediately after the minus, and at the end of the label,
1189 respectively
1190 - decimalsep, thousandsep, and thousandthpartsep (strings)
1191 are used as separators
1192 - minus (string) is inserted for negative numbers
1193 - period (string) is taken as a format string generating a period;
1194 it has to contain exactly one string insert operators "%s" for the
1195 period; usually it should be r"\overline{%s}"
1196 - labelattrs is a sequence of attributes for a texrunners text method;
1197 a single is allowed without being a sequence; None is considered as
1198 an empty sequence"""
1199 self.prefix = prefix
1200 self.infix = infix
1201 self.suffix = suffix
1202 self.equalprecision = equalprecision
1203 self.decimalsep = decimalsep
1204 self.thousandsep = thousandsep
1205 self.thousandthpartsep = thousandthpartsep
1206 self.minus = minus
1207 self.period = period
1208 self.labelattrs = helper.ensurelist(labelattrs)
1210 def labels(self, ticks):
1211 labeledticks = []
1212 maxdecprecision = 0
1213 for tick in ticks:
1214 if tick.label is None and tick.labellevel is not None:
1215 labeledticks.append(tick)
1216 m, n = tick.enum, tick.denom
1217 if m < 0: m *= -1
1218 if n < 0: n *= -1
1219 whole, reminder = divmod(m, n)
1220 whole = str(whole)
1221 if len(self.thousandsep):
1222 l = len(whole)
1223 tick.label = ""
1224 for i in range(l):
1225 tick.label += whole[i]
1226 if not ((l-i-1) % 3) and l > i+1:
1227 tick.label += self.thousandsep
1228 else:
1229 tick.label = whole
1230 if reminder:
1231 tick.label += self.decimalsep
1232 oldreminders = []
1233 tick.temp_decprecision = 0
1234 while (reminder):
1235 tick.temp_decprecision += 1
1236 if reminder in oldreminders:
1237 tick.temp_decprecision = None
1238 periodstart = len(tick.label) - (len(oldreminders) - oldreminders.index(reminder))
1239 tick.label = tick.label[:periodstart] + self.period % tick.label[periodstart:]
1240 break
1241 oldreminders += [reminder]
1242 reminder *= 10
1243 whole, reminder = divmod(reminder, n)
1244 if not ((tick.temp_decprecision - 1) % 3) and tick.temp_decprecision > 1:
1245 tick.label += self.thousandthpartsep
1246 tick.label += str(whole)
1247 if maxdecprecision < tick.temp_decprecision:
1248 maxdecprecision = tick.temp_decprecision
1249 if self.equalprecision:
1250 for tick in labeledticks:
1251 if tick.temp_decprecision is not None:
1252 if tick.temp_decprecision == 0 and maxdecprecision > 0:
1253 tick.label += self.decimalsep
1254 for i in range(tick.temp_decprecision, maxdecprecision):
1255 if not ((i - 1) % 3) and i > 1:
1256 tick.label += self.thousandthpartsep
1257 tick.label += "0"
1258 for tick in labeledticks:
1259 if tick.enum * tick.denom < 0:
1260 minus = self.minus
1261 else:
1262 minus = ""
1263 tick.label = "%s%s%s%s%s" % (self.prefix, minus, self.infix, tick.label, self.suffix)
1264 tick.labelattrs.extend(self.labelattrs)
1266 # del tick.temp_decprecision # we've inserted this temporary variable ... and do not care any longer about it
1269 class exponentialtexter:
1270 "a texter creating labels with exponentials (e.g. '2\cdot10^5')"
1272 __implements__ = _Itexter
1274 def __init__(self, plus="", minus="-",
1275 mantissaexp=r"{{%s}\cdot10^{%s}}",
1276 nomantissaexp=r"{10^{%s}}",
1277 minusnomantissaexp=r"{-10^{%s}}",
1278 mantissamin=frac((1, 1)), mantissamax=frac((10, 1)),
1279 skipmantissa1=0, skipallmantissa1=1,
1280 mantissatexter=decimaltexter()):
1281 r"""initializes the instance
1282 - plus or minus (string) is inserted for positive or negative exponents
1283 - mantissaexp (string) is taken as a format string generating the exponent;
1284 it has to contain exactly two string insert operators "%s" --
1285 the first for the mantissa and the second for the exponent;
1286 examples are r"{{%s}\cdot10^{%s}}" and r"{{%s}{\rm e}^{%s}}"
1287 - nomantissaexp (string) is taken as a format string generating the exponent
1288 when the mantissa is one and should be skipped; it has to contain
1289 exactly one string insert operators "%s" for the exponent;
1290 an examples is r"{10^{%s}}"
1291 - minusnomantissaexp (string) is taken as a format string generating the exponent
1292 when the mantissa is minus one and should be skipped; it has to contain
1293 exactly one string insert operators "%s" for the exponent; might be set to None
1294 to disallow skipping of any mantissa minus one
1295 an examples is r"{-10^{%s}}"
1296 - mantissamin and mantissamax are the minimum and maximum of the mantissa;
1297 they are frac instances greater than zero and mantissamin < mantissamax;
1298 the sign of the tick is ignored here
1299 - skipmantissa1 (boolean) turns on skipping of any mantissa equals one
1300 (and minus when minusnomantissaexp is set)
1301 - skipallmantissa1 (boolean) as above, but all mantissas must be 1
1302 - mantissatexter is the texter for the mantissa"""
1303 self.plus = plus
1304 self.minus = minus
1305 self.mantissaexp = mantissaexp
1306 self.nomantissaexp = nomantissaexp
1307 self.minusnomantissaexp = minusnomantissaexp
1308 self.mantissamin = mantissamin
1309 self.mantissamax = mantissamax
1310 self.mantissamindivmax = self.mantissamin / self.mantissamax
1311 self.mantissamaxdivmin = self.mantissamax / self.mantissamin
1312 self.skipmantissa1 = skipmantissa1
1313 self.skipallmantissa1 = skipallmantissa1
1314 self.mantissatexter = mantissatexter
1316 def labels(self, ticks):
1317 labeledticks = []
1318 for tick in ticks:
1319 if tick.label is None and tick.labellevel is not None:
1320 tick.temp_orgenum, tick.temp_orgdenom = tick.enum, tick.denom
1321 labeledticks.append(tick)
1322 tick.temp_exp = 0
1323 if tick.enum:
1324 while abs(tick) >= self.mantissamax:
1325 tick.temp_exp += 1
1326 x = tick * self.mantissamindivmax
1327 tick.enum, tick.denom = x.enum, x.denom
1328 while abs(tick) < self.mantissamin:
1329 tick.temp_exp -= 1
1330 x = tick * self.mantissamaxdivmin
1331 tick.enum, tick.denom = x.enum, x.denom
1332 if tick.temp_exp < 0:
1333 tick.temp_exp = "%s%i" % (self.minus, -tick.temp_exp)
1334 else:
1335 tick.temp_exp = "%s%i" % (self.plus, tick.temp_exp)
1336 self.mantissatexter.labels(labeledticks)
1337 if self.minusnomantissaexp is not None:
1338 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if abs(tick.enum) == abs(tick.denom)])
1339 else:
1340 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if tick.enum == tick.denom])
1341 for tick in labeledticks:
1342 if (self.skipallmantissa1 and allmantissa1 or
1343 (self.skipmantissa1 and (tick.enum == tick.denom or
1344 (tick.enum == -tick.denom and self.minusnomantissaexp is not None)))):
1345 if tick.enum == tick.denom:
1346 tick.label = self.nomantissaexp % tick.temp_exp
1347 else:
1348 tick.label = self.minusnomantissaexp % tick.temp_exp
1349 else:
1350 tick.label = self.mantissaexp % (tick.label, tick.temp_exp)
1351 tick.enum, tick.denom = tick.temp_orgenum, tick.temp_orgdenom
1353 # del tick.temp_orgenum # we've inserted those temporary variables ... and do not care any longer about them
1354 # del tick.temp_orgdenom
1355 # del tick.temp_exp
1358 class defaulttexter:
1359 "a texter creating decimal or exponential labels"
1361 __implements__ = _Itexter
1363 def __init__(self, smallestdecimal=frac((1, 1000)),
1364 biggestdecimal=frac((9999, 1)),
1365 equaldecision=1,
1366 decimaltexter=decimaltexter(),
1367 exponentialtexter=exponentialtexter()):
1368 r"""initializes the instance
1369 - smallestdecimal and biggestdecimal are the smallest and
1370 biggest decimal values, where the decimaltexter should be used;
1371 they are frac instances; the sign of the tick is ignored here;
1372 a tick at zero is considered for the decimaltexter as well
1373 - equaldecision (boolean) uses decimaltexter or exponentialtexter
1374 globaly (set) or for each tick separately (unset)
1375 - decimaltexter and exponentialtexter are texters to be used"""
1376 self.smallestdecimal = smallestdecimal
1377 self.biggestdecimal = biggestdecimal
1378 self.equaldecision = equaldecision
1379 self.decimaltexter = decimaltexter
1380 self.exponentialtexter = exponentialtexter
1382 def labels(self, ticks):
1383 decticks = []
1384 expticks = []
1385 for tick in ticks:
1386 if tick.label is None and tick.labellevel is not None:
1387 if not tick.enum or (abs(tick) >= self.smallestdecimal and abs(tick) <= self.biggestdecimal):
1388 decticks.append(tick)
1389 else:
1390 expticks.append(tick)
1391 if self.equaldecision:
1392 if len(expticks):
1393 self.exponentialtexter.labels(ticks)
1394 else:
1395 self.decimaltexter.labels(ticks)
1396 else:
1397 for tick in decticks:
1398 self.decimaltexter.labels([tick])
1399 for tick in expticks:
1400 self.exponentialtexter.labels([tick])
1403 ################################################################################
1404 # axis painter
1405 ################################################################################
1408 class axiscanvas(canvas._canvas):
1409 """axis canvas
1410 - an axis canvas is a regular canvas to be filled by
1411 a axispainters painter method
1412 - it contains extent (a pyx length) to be used for the
1413 alignment of additional axes; the axis extent should
1414 be filled by the axispainters painter method; you may
1415 grasp this as a size information comparable to a bounding
1417 - it contains labels (a list of textboxes) to be used to rate the
1418 distances between the labels if needed by the axis later on;
1419 the painter method has not only to insert the labels into this
1420 canvas, but should also fill this list"""
1422 # __implements__ = sole implementation
1424 def __init__(self, texrunner, *args, **kwargs):
1425 """initializes the instance
1426 - sets extent to zero
1427 - sets labels to an empty list"""
1428 canvas._canvas.__init__(self, *args, **kwargs)
1429 self.settexrunner(texrunner)
1430 self.extent = 0
1431 self.labels = []
1434 class rotatetext:
1435 """create rotations accordingly to tick directions
1436 - upsidedown rotations are suppressed by rotating them by another 180 degree"""
1438 # __implements__ = sole implementation
1440 def __init__(self, direction, epsilon=1e-10):
1441 """initializes the instance
1442 - direction is an angle to be used relative to the tick direction
1443 - epsilon is the value by which 90 degrees can be exceeded before
1444 an 180 degree rotation is added"""
1445 self.direction = direction
1446 self.epsilon = epsilon
1448 def trafo(self, dx, dy):
1449 """returns a rotation transformation accordingly to the tick direction
1450 - dx and dy are the direction of the tick"""
1451 direction = self.direction + math.atan2(dy, dx) * 180 / math.pi
1452 while (direction > 90 + self.epsilon):
1453 direction -= 180
1454 while (direction < -90 - self.epsilon):
1455 direction += 180
1456 return trafomodule.rotate(direction)
1459 paralleltext = rotatetext(-90)
1460 orthogonaltext = rotatetext(0)
1463 class _Iaxispainter:
1464 "class for painting axes"
1466 def paint(self, axis, axiscanvas):
1467 """paint the axis into the axiscanvas
1468 - the axis and should not be modified (we may
1469 add some temporary variables like axis.ticks[i].temp_xxx,
1470 which might be used just temporary) -- the idea is that
1471 all things can be used several times
1472 - also do not modify the instance (self) -- even this
1473 instance might be used several times; thus do not modify
1474 attributes like self.titleattrs etc. (just use a copy of it)
1475 - the axis should be accessed to get the tick positions
1476 namely by _Iaxispos and _Imap; see those interface
1477 definitions for details
1478 - the method might access some additional attributes from
1479 the axis, e.g. the axis title -- the axis painter should
1480 document this behavior and rely on the availability of
1481 those attributes -> it becomes a question of the proper
1482 usage of the combination of axis & axispainter
1483 - the axiscanvas is a axiscanvas instance and should be
1484 filled with ticks, labels, title, etc.; note that the
1485 extent and labels instance variables should be filled as
1486 documented in the axiscanvas"""
1489 class axistitlepainter:
1490 """class for painting an axis title
1491 - the axis must have a title attribute when using this painter;
1492 this title might be None"""
1494 __implements__ = _Iaxispainter
1496 def __init__(self, titledist="0.3 cm",
1497 titleattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1498 titledirection=paralleltext,
1499 titlepos=0.5):
1500 """initialized the instance
1501 - titledist is a visual PyX length giving the distance
1502 of the title from the axis extent already there (a title might
1503 be added after labels or other things are plotted already)
1504 - labelattrs is a sequence of attributes for a texrunners text
1505 method; a single is allowed without being a sequence; None
1506 turns off the title
1507 - titledirection is an instance of rotatetext or None
1508 - titlepos is the position of the title in graph coordinates"""
1509 self.titledist_str = titledist
1510 self.titleattrs = titleattrs
1511 self.titledirection = titledirection
1512 self.titlepos = titlepos
1514 def paint(self, axis, axiscanvas):
1515 if axis.title is not None and self.titleattrs is not None:
1516 titledist = unit.length(self.titledist_str, default_type="v")
1517 x, y = axis._vtickpoint(self.titlepos, axis=axis)
1518 dx, dy = axis.vtickdirection(self.titlepos, axis=axis)
1519 titleattrs = helper.ensurelist(self.titleattrs)
1520 if self.titledirection is not None:
1521 titleattrs.append(self.titledirection.trafo(dx, dy))
1522 title = axiscanvas.texrunner._text(x, y, axis.title, *titleattrs)
1523 axiscanvas.extent += titledist
1524 title.linealign(axiscanvas.extent, dx, dy)
1525 axiscanvas.extent += title.extent(dx, dy)
1526 axiscanvas.insert(title)
1529 class axispainter(axistitlepainter):
1530 """class for painting the ticks and labels of an axis
1531 - the inherited titleaxispainter is used to paint the title of
1532 the axis as well
1533 - note that the type of the elements of ticks given as an argument
1534 of the paint method must be suitable for the tick position methods
1535 of the axis"""
1537 __implements__ = _Iaxispainter
1539 defaultticklengths = ["%0.5f cm" % (0.2*goldenmean**(-i)) for i in range(10)]
1541 def __init__(self, innerticklengths=defaultticklengths,
1542 outerticklengths=None,
1543 tickattrs=(),
1544 gridattrs=None,
1545 zerolineattrs=(),
1546 baselineattrs=canvas.linecap.square,
1547 labeldist="0.3 cm",
1548 labelattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1549 labeldirection=None,
1550 labelhequalize=0,
1551 labelvequalize=1,
1552 **kwargs):
1553 """initializes the instance
1554 - innerticklenths and outerticklengths are two sequences of
1555 visual PyX lengths for ticks, subticks, etc. plotted inside
1556 and outside of the graph; when a single value is given, it
1557 is used for all tick levels; None turns off ticks inside or
1558 outside of the graph
1559 - tickattrs are a sequence of stroke attributes for the ticks;
1560 a single entry is allowed without being a sequence; None turns
1561 off ticks
1562 - gridlineattrs are a sequence of sequences used as stroke
1563 attributes for ticks, subticks etc.; when a single sequence
1564 is given, it is used for ticks, subticks, etc.; a single
1565 entry is allowed without being a sequence; None turns off
1566 gridlines
1567 - zerolineattrs are a sequence of stroke attributes for a grid
1568 line at axis value zero; a single entry is allowed without
1569 being a sequence; None turns off the zeroline
1570 - baselineattrs are a sequence of stroke attributes for a grid
1571 line at axis value zero; a single entry is allowed without
1572 being a sequence; None turns off the baseline
1573 - labeldist is a visual PyX length for the distance of the labels
1574 from the axis baseline
1575 - labelattrs is a sequence of attributes for a texrunners text
1576 method; a single entry is allowed without being a sequence;
1577 None turns off the labels
1578 - titledirection is an instance of rotatetext or None
1579 - labelhequalize and labelvequalize (booleans) perform an equal
1580 alignment for straight vertical and horizontal axes, respectively
1581 - futher keyword arguments are passed to axistitlepainter"""
1582 # TODO: access to axis.divisor -- document, remove, ... ???
1583 self.innerticklengths_str = innerticklengths
1584 self.outerticklengths_str = outerticklengths
1585 self.tickattrs = tickattrs
1586 self.gridattrs = gridattrs
1587 self.zerolineattrs = zerolineattrs
1588 self.baselineattrs = baselineattrs
1589 self.labeldist_str = labeldist
1590 self.labelattrs = labelattrs
1591 self.labeldirection = labeldirection
1592 self.labelhequalize = labelhequalize
1593 self.labelvequalize = labelvequalize
1594 axistitlepainter.__init__(self, **kwargs)
1596 def paint(self, axis, axiscanvas):
1597 labeldist = unit.length(self.labeldist_str, default_type="v")
1598 for tick in axis.ticks:
1599 tick.temp_v = axis.convert(float(tick) * axis.divisor)
1600 tick.temp_x, tick.temp_y = axis._vtickpoint(tick.temp_v, axis=axis)
1601 tick.temp_dx, tick.temp_dy = axis.vtickdirection(tick.temp_v, axis=axis)
1603 # create & align tick.temp_labelbox
1604 for tick in axis.ticks:
1605 if tick.labellevel is not None:
1606 labelattrs = helper.getsequenceno(self.labelattrs, tick.labellevel)
1607 if labelattrs is not None:
1608 labelattrs = helper.ensurelist(labelattrs)[:]
1609 if self.labeldirection is not None:
1610 labelattrs.append(self.labeldirection.trafo(tick.temp_dx, tick.temp_dy))
1611 if tick.labelattrs is not None:
1612 labelattrs.extend(helper.ensurelist(tick.labelattrs))
1613 tick.temp_labelbox = axiscanvas.texrunner._text(tick.temp_x, tick.temp_y, tick.label, *labelattrs)
1614 if len(axis.ticks) > 1:
1615 equaldirection = 1
1616 for tick in axis.ticks[1:]:
1617 if tick.temp_dx != axis.ticks[0].temp_dx or tick.temp_dy != axis.ticks[0].temp_dy:
1618 equaldirection = 0
1619 else:
1620 equaldirection = 0
1621 if equaldirection and ((not axis.ticks[0].temp_dx and self.labelvequalize) or
1622 (not axis.ticks[0].temp_dy and self.labelhequalize)):
1623 if self.labelattrs is not None:
1624 box.linealignequal([tick.temp_labelbox for tick in axis.ticks if tick.labellevel is not None],
1625 labeldist, axis.ticks[0].temp_dx, axis.ticks[0].temp_dy)
1626 else:
1627 for tick in axis.ticks:
1628 if tick.labellevel is not None and self.labelattrs is not None:
1629 tick.temp_labelbox.linealign(labeldist, tick.temp_dx, tick.temp_dy)
1631 def mkv(arg):
1632 if helper.issequence(arg):
1633 return [unit.length(a, default_type="v") for a in arg]
1634 if arg is not None:
1635 return unit.length(arg, default_type="v")
1636 innerticklengths = mkv(self.innerticklengths_str)
1637 outerticklengths = mkv(self.outerticklengths_str)
1639 for tick in axis.ticks:
1640 if tick.ticklevel is not None:
1641 innerticklength = helper.getitemno(innerticklengths, tick.ticklevel)
1642 outerticklength = helper.getitemno(outerticklengths, tick.ticklevel)
1643 if not (innerticklength is None and outerticklength is None):
1644 if innerticklength is None:
1645 innerticklength = 0
1646 if outerticklength is None:
1647 outerticklength = 0
1648 tickattrs = helper.getsequenceno(self.tickattrs, tick.ticklevel)
1649 if tickattrs is not None:
1650 _innerticklength = unit.topt(innerticklength)
1651 _outerticklength = unit.topt(outerticklength)
1652 x1 = tick.temp_x - tick.temp_dx * _innerticklength
1653 y1 = tick.temp_y - tick.temp_dy * _innerticklength
1654 x2 = tick.temp_x + tick.temp_dx * _outerticklength
1655 y2 = tick.temp_y + tick.temp_dy * _outerticklength
1656 axiscanvas.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(tickattrs))
1657 if tick != frac((0, 1)) or self.zerolineattrs is None:
1658 gridattrs = helper.getsequenceno(self.gridattrs, tick.ticklevel)
1659 if gridattrs is not None:
1660 axiscanvas.stroke(axis.vgridline(tick.temp_v, axis=axis), *helper.ensuresequence(gridattrs))
1661 if unit.topt(outerticklength) > unit.topt(axiscanvas.extent):
1662 axiscanvas.extent = outerticklength
1663 if unit.topt(-innerticklength) > unit.topt(axiscanvas.extent):
1664 axiscanvas.extent = -innerticklength
1665 if tick.labellevel is not None and self.labelattrs is not None:
1666 axiscanvas.insert(tick.temp_labelbox)
1667 axiscanvas.labels.append(tick.temp_labelbox)
1668 extent = tick.temp_labelbox.extent(tick.temp_dx, tick.temp_dy) + labeldist
1669 if unit.topt(extent) > unit.topt(axiscanvas.extent):
1670 axiscanvas.extent = extent
1671 if self.baselineattrs is not None:
1672 axiscanvas.stroke(axis.vbaseline(axis=axis), *helper.ensuresequence(self.baselineattrs))
1673 if self.zerolineattrs is not None:
1674 if len(axis.ticks) and axis.ticks[0] * axis.ticks[-1] < frac((0, 1)):
1675 axiscanvas.stroke(axis.gridline(0, axis=axis), *helper.ensuresequence(self.zerolineattrs))
1677 # for tick in axis.ticks:
1678 # del tick.temp_v # we've inserted those temporary variables ... and do not care any longer about them
1679 # del tick.temp_x
1680 # del tick.temp_y
1681 # del tick.temp_dx
1682 # del tick.temp_dy
1683 # if tick.labellevel is not None and self.labelattrs is not None:
1684 # del tick.temp_labelbox
1686 axistitlepainter.paint(self, axis, axiscanvas)
1689 class linkaxispainter(axispainter):
1690 """class for painting a linked axis
1691 - the inherited axispainter is used to paint the axis
1692 - modifies some constructor defaults"""
1694 __implements__ = _Iaxispainter
1696 def __init__(self, gridattrs=None,
1697 zerolineattrs=None,
1698 labelattrs=None,
1699 titleattrs=None,
1700 **kwargs):
1701 """initializes the instance
1702 - the gridattrs default is set to None thus skipping the girdlines
1703 - the zerolineattrs default is set to None thus skipping the zeroline
1704 - the labelattrs default is set to None thus skipping the labels
1705 - the titleattrs default is set to None thus skipping the title
1706 - all keyword arguments are passed to axispainter"""
1707 axispainter.__init__(self, gridattrs=gridattrs,
1708 zerolineattrs=zerolineattrs,
1709 labelattrs=labelattrs,
1710 titleattrs=titleattrs,
1711 **kwargs)
1714 class splitaxispainter(axistitlepainter):
1715 "class for painting a splitaxis"
1717 __implements__ = _Iaxispainter
1719 def __init__(self, breaklinesdist=0.05,
1720 breaklineslength=0.5,
1721 breaklinesangle=-60,
1722 breaklinesattrs=(),
1723 **args):
1724 self.breaklinesdist_str = breaklinesdist
1725 self.breaklineslength_str = breaklineslength
1726 self.breaklinesangle = breaklinesangle
1727 self.breaklinesattrs = breaklinesattrs
1728 axistitlepainter.__init__(self, **args)
1730 def paint(self, axis, axiscanvas):
1731 if self.breaklinesattrs is not None:
1732 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1733 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1734 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1735 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1736 breaklinesextent = (0.5*self.breaklinesdist*math.fabs(self.cos) +
1737 0.5*self.breaklineslength*math.fabs(self.sin))
1738 if unit.topt(axiscanvas.extent) < unit.topt(breaklinesextent):
1739 axiscanvas.extent = breaklinesextent
1740 for subaxis1, subaxis2 in zip(axis.axislist[:-1], axis.axislist[1:]):
1741 # use a tangent of the baseline (this is independent of the tickdirection)
1742 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1743 breakline = path.normpath(axis.vbaseline(v, None, axis=axis)).tangent(0, self.breaklineslength)
1744 widthline = path.normpath(axis.vbaseline(v, None, axis=axis)).tangent(0, self.breaklinesdist).transformed(trafomodule.rotate(self.breaklinesangle+90, *breakline.begin()))
1745 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1746 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1747 breakline = breakline.transformed(trafomodule.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1748 breakline1 = breakline.transformed(trafomodule.translate(*towidth))
1749 breakline2 = breakline.transformed(trafomodule.translate(-towidth[0], -towidth[1]))
1750 axiscanvas.fill(path.path(path.moveto(*breakline1.begin()),
1751 path.lineto(*breakline1.end()),
1752 path.lineto(*breakline2.end()),
1753 path.lineto(*breakline2.begin()),
1754 path.closepath()), color.gray.white)
1755 axiscanvas.stroke(breakline1, *helper.ensuresequence(self.breaklinesattrs))
1756 axiscanvas.stroke(breakline2, *helper.ensuresequence(self.breaklinesattrs))
1757 axistitlepainter.paint(self, axis, axiscanvas)
1760 class linksplitaxispainter(splitaxispainter):
1761 """class for painting a linked splitaxis axis
1762 - the inherited axispainter is used to paint the axis
1763 - modifies some constructor defaults"""
1765 __implements__ = _Iaxispainter
1767 def __init__(self, titleattrs=None, **kwargs):
1768 """initializes the instance
1769 - the titleattrs default is set to None thus skipping the title
1770 - all keyword arguments are passed to axispainter"""
1771 splitaxispainter.__init__(self, titleattrs=titleattrs, **kwargs)
1774 class baraxispainter(axistitlepainter): pass
1775 # class baraxispainter(axistitlepainter):
1777 # def __init__(self, innerticklength=None,
1778 # outerticklength=None,
1779 # tickattrs=(),
1780 # baselineattrs=canvas.linecap.square,
1781 # namedist="0.3 cm",
1782 # nameattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1783 # namedirection=None,
1784 # namepos=0.5,
1785 # namehequalize=0,
1786 # namevequalize=1,
1787 # **args):
1788 # self.innerticklength_str = innerticklength
1789 # self.outerticklength_str = outerticklength
1790 # self.tickattrs = tickattrs
1791 # self.baselineattrs = baselineattrs
1792 # self.namedist_str = namedist
1793 # self.nameattrs = nameattrs
1794 # self.namedirection = namedirection
1795 # self.namepos = namepos
1796 # self.namehequalize = namehequalize
1797 # self.namevequalize = namevequalize
1798 # axistitlepainter.__init__(self, **args)
1800 # def dolayout(self, graph, axis):
1801 # axis.layoutdata._extent = 0
1802 # if axis.multisubaxis:
1803 # for name, subaxis in zip(axis.names, axis.subaxis):
1804 # subaxis.vmin = axis.convert((name, 0))
1805 # subaxis.vmax = axis.convert((name, 1))
1806 # subaxis.baseaxis = axis
1807 # subaxis._vtickpoint = lambda axis, v: axis.baseaxis._vtickpoint(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1808 # subaxis.vtickdirection = lambda axis, v: axis.baseaxis.vtickdirection(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1809 # subaxis.vbaseline = None
1810 # subaxis.dolayout(graph)
1811 # if axis.layoutdata._extent < subaxis.layoutdata._extent:
1812 # axis.layoutdata._extent = subaxis.layoutdata._extent
1813 # axis.namepos = []
1814 # for name in axis.names:
1815 # v = axis.convert((name, self.namepos))
1816 # x, y = axis._vtickpoint(axis, v)
1817 # dx, dy = axis.vtickdirection(axis, v)
1818 # axis.namepos.append((v, x, y, dx, dy))
1819 # axis.nameboxes = []
1820 # if self.nameattrs is not None:
1821 # for (v, x, y, dx, dy), name in zip(axis.namepos, axis.names):
1822 # nameattrs = helper.ensurelist(self.nameattrs)
1823 # if self.namedirection is not None:
1824 # nameattrs += [trafomodule.rotate(self.reldirection(self.namedirection, dx, dy))]
1825 # if axis.texts.has_key(name):
1826 # axis.nameboxes.append(graph.texrunner._text(x, y, str(axis.texts[name]), *nameattrs))
1827 # elif axis.texts.has_key(str(name)):
1828 # axis.nameboxes.append(graph.texrunner._text(x, y, str(axis.texts[str(name)]), *nameattrs))
1829 # else:
1830 # axis.nameboxes.append(graph.texrunner._text(x, y, str(name), *nameattrs))
1831 # labeldist = axis.layoutdata._extent + unit.topt(unit.length(self.namedist_str, default_type="v"))
1832 # if len(axis.namepos) > 1:
1833 # equaldirection = 1
1834 # for namepos in axis.namepos[1:]:
1835 # if namepos[3] != axis.namepos[0][3] or namepos[4] != axis.namepos[0][4]:
1836 # equaldirection = 0
1837 # else:
1838 # equaldirection = 0
1839 # if equaldirection and ((not axis.namepos[0][3] and self.namevequalize) or
1840 # (not axis.namepos[0][4] and self.namehequalize)):
1841 # box._linealignequal(axis.nameboxes, labeldist, axis.namepos[0][3], axis.namepos[0][4])
1842 # else:
1843 # for namebox, namepos in zip(axis.nameboxes, axis.namepos):
1844 # namebox._linealign(labeldist, namepos[3], namepos[4])
1845 # if self.innerticklength_str is not None:
1846 # axis.innerticklength = unit.topt(unit.length(self.innerticklength_str, default_type="v"))
1847 # else:
1848 # if self.outerticklength_str is not None:
1849 # axis.innerticklength = 0
1850 # else:
1851 # axis.innerticklength = None
1852 # if self.outerticklength_str is not None:
1853 # axis.outerticklength = unit.topt(unit.length(self.outerticklength_str, default_type="v"))
1854 # else:
1855 # if self.innerticklength_str is not None:
1856 # axis.outerticklength = 0
1857 # else:
1858 # axis.outerticklength = None
1859 # if axis.outerticklength is not None and self.tickattrs is not None:
1860 # axis.layoutdata._extent += axis.outerticklength
1861 # for (v, x, y, dx, dy), namebox in zip(axis.namepos, axis.nameboxes):
1862 # newextent = namebox._extent(dx, dy) + labeldist
1863 # if axis.layoutdata._extent < newextent:
1864 # axis.layoutdata._extent = newextent
1865 # #graph.mindbbox(*[namebox.bbox() for namebox in axis.nameboxes])
1866 # axistitlepainter.dolayout(self, graph, axis)
1868 # def paint(self, graph, axis):
1869 # if axis.subaxis is not None:
1870 # if axis.multisubaxis:
1871 # for subaxis in axis.subaxis:
1872 # subaxis.dopaint(graph)
1873 # if None not in (self.tickattrs, axis.innerticklength, axis.outerticklength):
1874 # for pos in axis.relsizes:
1875 # if pos == axis.relsizes[0]:
1876 # pos -= axis.firstdist
1877 # elif pos != axis.relsizes[-1]:
1878 # pos -= 0.5 * axis.dist
1879 # v = pos / axis.relsizes[-1]
1880 # x, y = axis._vtickpoint(axis, v)
1881 # dx, dy = axis.vtickdirection(axis, v)
1882 # x1 = x - dx * axis.innerticklength
1883 # y1 = y - dy * axis.innerticklength
1884 # x2 = x + dx * axis.outerticklength
1885 # y2 = y + dy * axis.outerticklength
1886 # graph.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(self.tickattrs))
1887 # if self.baselineattrs is not None:
1888 # if axis.vbaseline is not None: # XXX: subbaselines (as for splitlines)
1889 # graph.stroke(axis.vbaseline(axis), *helper.ensuresequence(self.baselineattrs))
1890 # for namebox in axis.nameboxes:
1891 # graph.insert(namebox)
1892 # axistitlepainter.paint(self, graph, axis)
1896 ################################################################################
1897 # axes
1898 ################################################################################
1901 class _Iaxis:
1902 """interface definition of a axis
1903 - data and tick range are handled separately
1904 - an axis implements _Imap as well to convert between
1905 axis values and graph coordinates; this is usually done
1906 by mixin a proper mapping
1907 - instance variables:
1908 - axiscanvas: an axiscanvas instance, which is available after
1909 calling the finish method
1910 - relsize: relative size (width) of the axis (for use
1911 in splitaxis etc.) -- this might not be available for
1912 all axes, which will just create an name error right now
1913 - configuration of the range handling is not subject
1914 of this interface definition"""
1915 # TODO: - add a mechanism to allow for a different range
1916 # handling of x and y ranges
1917 # - should we document instance variables this way?
1919 __implements__ = _Imap
1921 def setdatarange(self, min, max):
1922 """set the axis data range
1923 - the type of min and max must fit to the axis
1924 - min<max; the axis might be reversed, but this is
1925 expressed internally only
1926 - the axis might not apply this change of the range
1927 (e.g. when the axis range is fixed by the user)
1928 - for invalid parameters (e.g. negativ values at an
1929 logarithmic axis), an exception should be raised"""
1930 # TODO: be more specific about the type of the exception
1932 def settickrange(self, min, max):
1933 """set the axis tick range
1934 - as before, but for the tick range"""
1936 def getdatarange(self):
1937 """return data range as a tuple (min, max)
1938 - min<max; the axis might be reversed, but this is
1939 expressed internally only"""
1940 # TODO: exceptions???
1942 def gettickrange(self):
1943 """return tick range as a tuple (min, max)
1944 - as before, but for the tick range"""
1945 # TODO: exceptions???
1947 def finish(self, graph):
1948 """paint the axis into the graph
1949 - the graph should not be modified (read-only); the only
1950 important property of the graph should be its texrunner
1951 instance
1952 - the axis instance (self) should be accessed to get the
1953 tick positions namely by _Iaxispos and _Imap; see
1954 those interface definitions for details -- again, the
1955 graph should not be needed for that
1956 - the finish method creates an axiscanvas, which should be
1957 insertable into the graph to finally paint the axis
1958 - any modification of the axis range should be disabled after
1959 the finish method was called; a possible implementation would
1960 be to raise an error in these methods as soon as an axiscanvas
1961 is available"""
1964 class _Iaxispos:
1965 """interface definition of axis tick position methods
1966 - these methods are used for the postitioning of the ticks
1967 when painting an axis
1968 - a graph should insert these methods appropriate to its design
1969 before the painting of the axis takes place, thus self is the
1970 graph instance and the axis must be provided as a separate
1971 argument"""
1973 def baseline(self, x1=None, x2=None, axis=None):
1974 """return the baseline as a path
1975 - x1 is the start position; if not set, the baseline starts
1976 from the beginning of the axis, which might imply a
1977 value outside of the graph coordinate range [0; 1]
1978 - x2 is analogous to x1, but for the end position"""
1980 def vbaseline(self, v1=None, v2=None, axis=None):
1981 """return the baseline as a path
1982 - like baseline, but for graph coordinates"""
1984 def gridline(self, x, axis=None):
1985 "return the gridline as a path for a given position x"
1987 def vgridline(self, v, axis=None):
1988 """return the gridline as a path for a given position v
1989 in graph coordinates"""
1991 def _tickpoint(self, x, axis=None):
1992 """return the position at the baseline as a tuple (x, y) in
1993 postscript points for the position x"""
1995 def tickpoint(self, x, axis=None):
1996 """return the position at the baseline as a tuple (x, y) in
1997 in PyX length for the position x"""
1999 def _vtickpoint(self, v, axis=None):
2000 "like _tickpoint, but for graph coordinates"
2002 def vtickpoint(self, v, axis=None):
2003 "like tickpoint, but for graph coordinates"
2005 def tickdirection(self, x, axis=None):
2006 """return the direction of a tick as a tuple (dx, dy) for the
2007 position x"""
2009 def vtickdirection(self, v, axis=None):
2010 """like tickposition, but for graph coordinates"""
2013 class _axis:
2015 __implements__ = _Iaxis
2017 def __init__(self, min=None, max=None, reverse=0, divisor=1,
2018 datavmin=None, datavmax=None, tickvmin=0, tickvmax=1,
2019 title=None, painter=axispainter(), texter=defaulttexter(),
2020 density=1, maxworse=2):
2021 # - a axis has a part attribute where it stores a partitioner or/and some
2022 # (manually set) ticks -> the part attribute is used to create the ticks
2023 # in the axis finish method
2024 if None not in (min, max) and min > max:
2025 min, max, reverse = max, min, not reverse
2026 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
2028 self.datamin = self.datamax = self.tickmin = self.tickmax = None
2029 if datavmin is None:
2030 if self.fixmin:
2031 self.datavmin = 0
2032 else:
2033 self.datavmin = 0.05
2034 else:
2035 self.datavmin = datavmin
2036 if datavmax is None:
2037 if self.fixmax:
2038 self.datavmax = 1
2039 else:
2040 self.datavmax = 0.95
2041 else:
2042 self.datavmax = datavmax
2043 self.tickvmin = tickvmin
2044 self.tickvmax = tickvmax
2046 self.divisor = divisor
2047 self.title = title
2048 self.painter = painter
2049 self.texter = texter
2050 self.density = density
2051 self.maxworse = maxworse
2052 self.axiscanvas = None
2053 self.canconvert = 0
2054 self.finished = 0
2055 self.__setinternalrange()
2057 def __setinternalrange(self, min=None, max=None):
2058 if not self.fixmin and min is not None and (self.min is None or min < self.min):
2059 self.min = min
2060 if not self.fixmax and max is not None and (self.max is None or max > self.max):
2061 self.max = max
2062 if None not in (self.min, self.max):
2063 min, max, vmin, vmax = self.min, self.max, 0, 1
2064 self.canconvert = 1
2065 self.setbasepoints(((min, vmin), (max, vmax)))
2066 if not self.fixmin:
2067 if self.datamin is not None and self.convert(self.datamin) < self.datavmin:
2068 min, vmin = self.datamin, self.datavmin
2069 self.setbasepoints(((min, vmin), (max, vmax)))
2070 if self.tickmin is not None and self.convert(self.tickmin) < self.tickvmin:
2071 min, vmin = self.tickmin, self.tickvmin
2072 self.setbasepoints(((min, vmin), (max, vmax)))
2073 if not self.fixmax:
2074 if self.datamax is not None and self.convert(self.datamax) > self.datavmax:
2075 max, vmax = self.datamax, self.datavmax
2076 self.setbasepoints(((min, vmin), (max, vmax)))
2077 if self.tickmax is not None and self.convert(self.tickmax) > self.tickvmax:
2078 max, vmax = self.tickmax, self.tickvmax
2079 self.setbasepoints(((min, vmin), (max, vmax)))
2080 if self.reverse:
2081 self.setbasepoints(((min, vmax), (max, vmin)))
2083 def __getinternalrange(self):
2084 return self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax
2086 def __forceinternalrange(self, range):
2087 self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax = range
2088 self.__setinternalrange()
2090 def setdatarange(self, min, max):
2091 if self.axiscanvas is not None:
2092 raise RuntimeError("axis was already finished")
2093 self.datamin, self.datamax = min, max
2094 self.__setinternalrange(min, max)
2096 def settickrange(self, min, max):
2097 if self.axiscanvas is not None:
2098 raise RuntimeError("axis was already finished")
2099 self.tickmin, self.tickmax = min, max
2100 self.__setinternalrange(min, max)
2102 def getdatarange(self):
2103 if self.canconvert:
2104 if self.reverse:
2105 return self.invert(1-self.datavmin), self.invert(1-self.datavmax)
2106 else:
2107 return self.invert(self.datavmin), self.invert(self.datavmax)
2109 def gettickrange(self):
2110 if self.canconvert:
2111 if self.reverse:
2112 return self.invert(1-self.tickvmin), self.invert(1-self.tickvmax)
2113 else:
2114 return self.invert(self.tickvmin), self.invert(self.tickvmax)
2116 def checkfraclist(self, fracs):
2117 "orders a list of fracs, equal entries are not allowed"
2118 if not len(fracs): return []
2119 sorted = list(fracs)
2120 sorted.sort()
2121 last = sorted[0]
2122 for item in sorted[1:]:
2123 if last == item:
2124 raise ValueError("duplicate entry found")
2125 last = item
2126 return sorted
2128 def finish(self, graph):
2129 if self.finished:
2130 return
2131 self.finished = 1
2133 min, max = self.gettickrange()
2134 if self.part is not None:
2135 parter = parterpos = None
2136 if helper.issequence(self.part):
2137 for p, i in zip(self.part, xrange(sys.maxint)):
2138 if hasattr(p, "defaultpart"):
2139 if parter is not None:
2140 raise RuntimeError("only one partitioner allowed")
2141 parter = p
2142 parterpos = i
2143 self.part[:parterpos] = self.checkfraclist(self.part[:parterpos])
2144 self.part[parterpos+1:] = self.checkfraclist(self.part[parterpos+1:])
2145 else:
2146 parter = self.part
2147 if parter is not None:
2148 self.ticks = parter.defaultpart(min/self.divisor,
2149 max/self.divisor,
2150 not self.fixmin,
2151 not self.fixmax)
2152 if parterpos is not None:
2153 self.ticks = _mergeticklists(_mergeticklists(self.part[:parterpos], self.ticks), self.part[parterpos+1:])
2154 else:
2155 self.ticks = []
2156 # lesspart and morepart can be called after defaultpart;
2157 # this works although some axes may share their autoparting,
2158 # because the axes are processed sequentially
2159 first = 1
2160 worse = 0
2161 while worse < self.maxworse:
2162 if parter is not None:
2163 newticks = parter.lesspart()
2164 if parterpos is not None:
2165 newticks = _mergeticklists(_mergeticklists(self.part[:parterpos], newticks), self.part[parterpos+1:])
2166 else:
2167 newticks = None
2168 if newticks is not None:
2169 if first:
2170 bestrate = self.rater.ratepart(self, self.ticks, self.density)
2171 variants = [[bestrate, self.ticks]]
2172 first = 0
2173 newrate = self.rater.ratepart(self, newticks, self.density)
2174 variants.append([newrate, newticks])
2175 if newrate < bestrate:
2176 bestrate = newrate
2177 worse = 0
2178 else:
2179 worse += 1
2180 else:
2181 worse += 1
2182 worse = 0
2183 while worse < self.maxworse:
2184 if parter is not None:
2185 newticks = parter.morepart()
2186 if parterpos is not None:
2187 newticks = _mergeticklists(_mergeticklists(self.part[:parterpos], newticks), self.part[parterpos+1:])
2188 else:
2189 newticks = None
2190 if newticks is not None:
2191 if first:
2192 bestrate = self.rater.ratepart(self, self.ticks, self.density)
2193 variants = [[bestrate, self.ticks]]
2194 first = 0
2195 newrate = self.rater.ratepart(self, newticks, self.density)
2196 variants.append([newrate, newticks])
2197 if newrate < bestrate:
2198 bestrate = newrate
2199 worse = 0
2200 else:
2201 worse += 1
2202 else:
2203 worse += 1
2205 if not first:
2206 variants.sort()
2207 if self.painter is not None:
2208 i = 0
2209 bestrate = None
2210 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
2211 saverange = self.__getinternalrange()
2212 self.ticks = variants[i][1]
2213 if len(self.ticks):
2214 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2215 ac = axiscanvas(graph.texrunner)
2216 self.texter.labels(self.ticks)
2217 self.painter.paint(self, ac)
2218 ratelayout = self.rater.ratelayout(ac, self.density)
2219 if ratelayout is not None:
2220 variants[i][0] += ratelayout
2221 variants[i].append(ac)
2222 else:
2223 variants[i][0] = None
2224 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
2225 bestrate = variants[i][0]
2226 self.__forceinternalrange(saverange)
2227 i += 1
2228 if bestrate is None:
2229 raise RuntimeError("no valid axis partitioning found")
2230 variants = [variant for variant in variants[:i] if variant[0] is not None]
2231 variants.sort()
2232 self.ticks = variants[0][1]
2233 if len(self.ticks):
2234 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2235 self.axiscanvas = variants[0][2]
2236 else:
2237 if len(self.ticks):
2238 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2239 self.axiscanvas = axiscanvas(graph.texrunner)
2240 else:
2241 if len(self.ticks):
2242 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2243 self.axiscanvas = axiscanvas(graph.texrunner)
2244 self.texter.labels(self.ticks)
2245 self.painter.paint(self, self.axiscanvas)
2247 def createlinkaxis(self, **args):
2248 return linkaxis(self, **args)
2251 class linaxis(_axis, _linmap):
2253 def __init__(self, part=autolinpart(), rater=axisrater(), **args):
2254 _axis.__init__(self, **args)
2255 if self.fixmin and self.fixmax:
2256 self.relsize = self.max - self.min
2257 self.part = part
2258 self.rater = rater
2261 class logaxis(_axis, _logmap):
2263 def __init__(self, part=autologpart(), rater=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
2264 _axis.__init__(self, **args)
2265 if self.fixmin and self.fixmax:
2266 self.relsize = math.log(self.max) - math.log(self.min)
2267 self.part = part
2268 self.rater = rater
2271 class linkaxis:
2273 def __init__(self, linkedaxis, painter=linkaxispainter()):
2274 self.linkedaxis = linkedaxis
2275 self.painter = painter
2276 self.finished = 0
2278 def __getattr__(self, attr):
2279 return getattr(self.linkedaxis, attr)
2281 def finish(self, graph):
2282 if self.finished:
2283 return
2284 self.finished = 1
2285 self.linkedaxis.finish(graph)
2286 self.axiscanvas = axiscanvas(graph.texrunner)
2287 self.painter.paint(self, self.axiscanvas)
2290 class _splitaxis:
2292 def __init__(self, axislist, painter=splitaxispainter()):
2293 self.axislist = axislist
2294 self.painter = painter
2295 self.finished = 0
2297 def subbaseline(self, x1=None, x2=None, axis=None):
2298 if x1 is not None:
2299 v1 = axis.convert(x1)
2300 else:
2301 v1 = None
2302 if x2 is not None:
2303 v2 = axis.convert(x2)
2304 else:
2305 v2 = None
2306 return self.subvbaseline(v1, v2, axis=axis)
2308 def subvbaseline(self, v1=None, v2=None, axis=None):
2309 if v1 is None:
2310 if self.painter.breaklinesattrs is None: # XXX undocumented access to painter!?
2311 left = axis.vmin
2312 else:
2313 if axis.vminover is None:
2314 left = None
2315 else:
2316 left = axis.vminover
2317 else:
2318 left = axis.vmin+v1*(axis.vmax-axis.vmin)
2319 if v2 is None:
2320 if self.painter.breaklinesattrs is None:
2321 right = axis.vmax
2322 else:
2323 if axis.vmaxover is None:
2324 right = None
2325 else:
2326 right = axis.vmaxover
2327 else:
2328 right = axis.vmin+v2*(axis.vmax-axis.vmin)
2329 return self.vbaseline(left, right, axis=self)
2331 def subgridline(self, x, axis=None):
2332 return self.vgridline(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=self)
2334 def subvgridline(self, v, axis=None):
2335 return self.vgridline(axis.vmin+v*(axis.vmax-axis.vmin), axis=self)
2337 def sub_tickpoint(self, x, axis=None):
2338 return self._vtickpoint(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=self)
2340 def subtickpoint(self, x, axis=None):
2341 return self.vtickpoint(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=self)
2343 def sub_vtickpoint(self, v, axis=None):
2344 return self._vtickpoint(axis.vmin+v*(axis.vmax-axis.vmin), axis=self)
2346 def subvtickpoint(self, v, axis=None):
2347 return self.vtickpoint(axis.vmin+v*(axis.vmax-axis.vmin), axis=self)
2349 def subtickdirection(self, x, axis=None):
2350 return self.vtickdirection(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=self)
2352 def subvtickdirection(self, v, axis=None):
2353 return self.vtickdirection(axis.vmin+v*(axis.vmax-axis.vmin), axis=self)
2355 def finish(self, graph):
2356 if self.finished:
2357 return
2358 self.finished = 1
2359 self.axiscanvas = axiscanvas(graph.texrunner)
2360 for subaxis in self.axislist:
2361 subaxis.baseline = self.subbaseline
2362 subaxis.vbaseline = self.subvbaseline
2363 subaxis.gridline = self.subgridline
2364 subaxis.vgridline = self.subvgridline
2365 subaxis._tickpoint = self.sub_tickpoint
2366 subaxis.tickpoint = self.subtickpoint
2367 subaxis._vtickpoint = self.sub_vtickpoint
2368 subaxis.vtickpoint = self.subvtickpoint
2369 subaxis.tickdirection = self.subtickdirection
2370 subaxis.vtickdirection = self.subvtickdirection
2371 subaxis.finish(graph)
2372 self.axiscanvas.insert(subaxis.axiscanvas)
2373 if unit.topt(self.axiscanvas.extent) < unit.topt(subaxis.axiscanvas.extent):
2374 self.axiscanvas.extent = subaxis.axiscanvas.extent
2375 self.painter.paint(self, self.axiscanvas)
2377 def createlinkaxis(self, **args):
2378 return linksplitaxis(self, **args)
2381 class splitaxis(_splitaxis):
2383 def __init__(self, axislist, splitlist=0.5, splitdist=0.1, relsizesplitdist=1, title=None, **kwargs):
2384 _splitaxis.__init__(self, axislist, **kwargs)
2385 self.title = title
2386 self.splitlist = list(helper.ensuresequence(splitlist))
2387 self.splitlist.sort()
2388 if len(self.axislist) != len(self.splitlist) + 1:
2389 for subaxis in self.axislist:
2390 if not isinstance(subaxis, linkaxis):
2391 raise ValueError("axislist and splitlist lengths do not fit together")
2392 for subaxis in self.axislist:
2393 if isinstance(subaxis, linkaxis):
2394 subaxis.vmin = subaxis.linkedaxis.vmin
2395 subaxis.vminover = subaxis.linkedaxis.vminover
2396 subaxis.vmax = subaxis.linkedaxis.vmax
2397 subaxis.vmaxover = subaxis.linkedaxis.vmaxover
2398 else:
2399 subaxis.vmin = None
2400 subaxis.vmax = None
2401 self.axislist[0].vmin = 0
2402 self.axislist[0].vminover = None
2403 self.axislist[-1].vmax = 1
2404 self.axislist[-1].vmaxover = None
2405 for i in xrange(len(self.splitlist)):
2406 if self.splitlist[i] is not None:
2407 self.axislist[i].vmax = self.splitlist[i] - 0.5*splitdist
2408 self.axislist[i].vmaxover = self.splitlist[i]
2409 self.axislist[i+1].vmin = self.splitlist[i] + 0.5*splitdist
2410 self.axislist[i+1].vminover = self.splitlist[i]
2411 i = 0
2412 while i < len(self.axislist):
2413 if self.axislist[i].vmax is None:
2414 j = relsize = relsize2 = 0
2415 while self.axislist[i + j].vmax is None:
2416 relsize += self.axislist[i + j].relsize + relsizesplitdist
2417 j += 1
2418 relsize += self.axislist[i + j].relsize
2419 vleft = self.axislist[i].vmin
2420 vright = self.axislist[i + j].vmax
2421 for k in range(i, i + j):
2422 relsize2 += self.axislist[k].relsize
2423 self.axislist[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
2424 relsize2 += 0.5 * relsizesplitdist
2425 self.axislist[k].vmaxover = self.axislist[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
2426 relsize2 += 0.5 * relsizesplitdist
2427 self.axislist[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
2428 if i == 0 and i + j + 1 == len(self.axislist):
2429 self.relsize = relsize
2430 i += j + 1
2431 else:
2432 i += 1
2434 self.fixmin = self.axislist[0].fixmin
2435 if self.fixmin:
2436 self.min = self.axislist[0].min
2437 self.fixmax = self.axislist[-1].fixmax
2438 if self.fixmax:
2439 self.max = self.axislist[-1].max
2441 def getdatarange(self):
2442 min = self.axislist[0].getdatarange()
2443 max = self.axislist[-1].getdatarange()
2444 try:
2445 return min[0], max[1]
2446 except TypeError:
2447 return None
2449 def setdatarange(self, min, max):
2450 self.axislist[0].setdatarange(min, None)
2451 self.axislist[-1].setdatarange(None, max)
2453 def gettickrange(self):
2454 min = self.axislist[0].gettickrange()
2455 max = self.axislist[-1].gettickrange()
2456 try:
2457 return min[0], max[1]
2458 except TypeError:
2459 return None
2461 def settickrange(self, min, max):
2462 self.axislist[0].settickrange(min, None)
2463 self.axislist[-1].settickrange(None, max)
2465 def convert(self, value):
2466 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2467 if value < self.axislist[0].max:
2468 return self.axislist[0].vmin + self.axislist[0].convert(value)*(self.axislist[0].vmax-self.axislist[0].vmin)
2469 for axis in self.axislist[1:-1]:
2470 if value > axis.min and value < axis.max:
2471 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
2472 if value > self.axislist[-1].min:
2473 return self.axislist[-1].vmin + self.axislist[-1].convert(value)*(self.axislist[-1].vmax-self.axislist[-1].vmin)
2474 raise ValueError("value couldn't be assigned to a split region")
2477 class linksplitaxis(_splitaxis):
2478 # caution here: some of the methods/variables are part
2479 # of self, other methods/variables are shared (thus, they
2480 # do *not* belong to the linked axis) -> this should be
2481 # well documented
2483 def __init__(self, linkedaxis, painter=linksplitaxispainter(), **kwargs):
2484 self.linkedaxis = linkedaxis
2485 _splitaxis.__init__(self, [a.createlinkaxis() for a in self.linkedaxis.axislist], painter=painter, **kwargs)
2487 def __getattr__(self, attr):
2488 return getattr(self.linkedaxis, attr)
2490 def finish(self, graph):
2491 self.linkedaxis.finish(graph)
2492 _splitaxis.finish(self, graph)
2495 class baraxis:
2497 def __init__(self, subaxis=None, multisubaxis=0, title=None, dist=0.5, firstdist=None, lastdist=None, names=None, texts={}, painter=baraxispainter()):
2498 self.dist = dist
2499 if firstdist is not None:
2500 self.firstdist = firstdist
2501 else:
2502 self.firstdist = 0.5 * dist
2503 if lastdist is not None:
2504 self.lastdist = lastdist
2505 else:
2506 self.lastdist = 0.5 * dist
2507 self.relsizes = None
2508 self.fixnames = 0
2509 self.names = []
2510 for name in helper.ensuresequence(names):
2511 self.setname(name)
2512 self.fixnames = names is not None
2513 self.multisubaxis = multisubaxis
2514 if self.multisubaxis:
2515 self.createsubaxis = subaxis
2516 self.subaxis = [self.createsubaxis.createsubaxis() for name in self.names]
2517 else:
2518 self.subaxis = subaxis
2519 self.title = title
2520 self.fixnames = 0
2521 self.texts = texts
2522 self.painter = painter
2524 def getdatarange(self):
2525 return None
2527 def setname(self, name, *subnames):
2528 # setting self.relsizes to None forces later recalculation
2529 if not self.fixnames:
2530 if name not in self.names:
2531 self.relsizes = None
2532 self.names.append(name)
2533 if self.multisubaxis:
2534 self.subaxis.append(self.createsubaxis.createsubaxis())
2535 if (not self.fixnames or name in self.names) and len(subnames):
2536 if self.multisubaxis:
2537 if self.subaxis[self.names.index(name)].setname(*subnames):
2538 self.relsizes = None
2539 else:
2540 #print self.subaxis, self.multisubaxis, subnames, name
2541 if self.subaxis.setname(*subnames):
2542 self.relsizes = None
2543 return self.relsizes is not None
2545 def updaterelsizes(self):
2546 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
2547 self.relsizes[-1] += self.lastdist - self.dist
2548 if self.multisubaxis:
2549 subrelsize = 0
2550 for i in range(1, len(self.relsizes)):
2551 self.subaxis[i-1].updaterelsizes()
2552 subrelsize += self.subaxis[i-1].relsizes[-1]
2553 self.relsizes[i] += subrelsize
2554 else:
2555 if self.subaxis is None:
2556 subrelsize = 1
2557 else:
2558 self.subaxis.updaterelsizes()
2559 subrelsize = self.subaxis.relsizes[-1]
2560 for i in range(1, len(self.relsizes)):
2561 self.relsizes[i] += i * subrelsize
2563 def convert(self, value):
2564 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2565 if not self.relsizes:
2566 self.updaterelsizes()
2567 pos = self.names.index(value[0])
2568 if len(value) == 2:
2569 if self.subaxis is None:
2570 subvalue = value[1]
2571 else:
2572 if self.multisubaxis:
2573 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
2574 else:
2575 subvalue = value[1] * self.subaxis.relsizes[-1]
2576 else:
2577 if self.multisubaxis:
2578 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
2579 else:
2580 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
2581 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
2583 def dolayout(self, graph):
2584 self.layoutdata = layoutdata()
2585 self.painter.dolayout(graph, self)
2587 def dopaint(self, graph):
2588 self.painter.paint(graph, self)
2590 def createlinkaxis(self, **args):
2591 if self.subaxis is not None:
2592 if self.multisubaxis:
2593 subaxis = [subaxis.createlinkaxis() for subaxis in self.subaxis]
2594 else:
2595 subaxis = self.subaxis.createlinkaxis()
2596 else:
2597 subaxis = None
2598 return baraxis(subaxis=subaxis, dist=self.dist, firstdist=self.firstdist, lastdist=self.lastdist, **args)
2600 createsubaxis = createlinkaxis
2603 ################################################################################
2604 # graph key
2605 ################################################################################
2608 # g = graph.graphxy(key=graph.key())
2609 # g.addkey(graph.key(), ...)
2612 class key:
2614 def __init__(self, dist="0.2 cm", pos = "tr", hinside = 1, vinside = 1, hdist="0.6 cm", vdist="0.4 cm",
2615 symbolwidth="0.5 cm", symbolheight="0.25 cm", symbolspace="0.2 cm",
2616 textattrs=textmodule.vshift.mathaxis):
2617 self.dist_str = dist
2618 self.pos = pos
2619 self.hinside = hinside
2620 self.vinside = vinside
2621 self.hdist_str = hdist
2622 self.vdist_str = vdist
2623 self.symbolwidth_str = symbolwidth
2624 self.symbolheight_str = symbolheight
2625 self.symbolspace_str = symbolspace
2626 self.textattrs = textattrs
2627 self.plotinfos = None
2628 if self.pos in ("tr", "rt"):
2629 self.right = 1
2630 self.top = 1
2631 elif self.pos in ("br", "rb"):
2632 self.right = 1
2633 self.top = 0
2634 elif self.pos in ("tl", "lt"):
2635 self.right = 0
2636 self.top = 1
2637 elif self.pos in ("bl", "lb"):
2638 self.right = 0
2639 self.top = 0
2640 else:
2641 raise RuntimeError("invalid pos attribute")
2643 def setplotinfos(self, *plotinfos):
2644 """set the plotinfos to be used in the key
2645 - call it exactly once"""
2646 if self.plotinfos is not None:
2647 raise RuntimeError("setplotinfo is called multiple times")
2648 self.plotinfos = plotinfos
2650 def dolayout(self, graph):
2651 "creates the layout of the key"
2652 self._dist = unit.topt(unit.length(self.dist_str, default_type="v"))
2653 self._hdist = unit.topt(unit.length(self.hdist_str, default_type="v"))
2654 self._vdist = unit.topt(unit.length(self.vdist_str, default_type="v"))
2655 self._symbolwidth = unit.topt(unit.length(self.symbolwidth_str, default_type="v"))
2656 self._symbolheight = unit.topt(unit.length(self.symbolheight_str, default_type="v"))
2657 self._symbolspace = unit.topt(unit.length(self.symbolspace_str, default_type="v"))
2658 self.titles = []
2659 for plotinfo in self.plotinfos:
2660 self.titles.append(graph.texrunner._text(0, 0, plotinfo.data.title, *helper.ensuresequence(self.textattrs)))
2661 box._tile(self.titles, self._dist, 0, -1)
2662 box._linealignequal(self.titles, self._symbolwidth + self._symbolspace, 1, 0)
2664 def bbox(self):
2665 """return a bbox for the key
2666 method should be called after dolayout"""
2667 result = self.titles[0].bbox()
2668 for title in self.titles[1:]:
2669 result = result + title.bbox() + bbox._bbox(0, title.center[1] - 0.5 * self._symbolheight,
2670 0, title.center[1] + 0.5 * self._symbolheight)
2671 return result
2673 def paint(self, c, x, y):
2674 """paint the graph key into a canvas c at the position x and y (in postscript points)
2675 - method should be called after dolayout
2676 - the x, y alignment might be calculated by the graph using:
2677 - the bbox of the key as returned by the keys bbox method
2678 - the attributes _hdist, _vdist, hinside, and vinside of the key
2679 - the dimension and geometry of the graph"""
2680 sc = c.insert(canvas.canvas(trafomodule._translate(x, y)))
2681 for plotinfo, title in zip(self.plotinfos, self.titles):
2682 plotinfo.style.key(sc, 0, -0.5 * self._symbolheight + title.center[1],
2683 self._symbolwidth, self._symbolheight)
2684 sc.insert(title)
2687 ################################################################################
2688 # graph
2689 ################################################################################
2692 class plotinfo:
2694 def __init__(self, data, style):
2695 self.data = data
2696 self.style = style
2699 class graphxy(canvas.canvas):
2701 Names = "x", "y"
2703 def clipcanvas(self):
2704 return self.insert(canvas.canvas(canvas.clip(path._rect(self._xpos, self._ypos, self._width, self._height))))
2706 def plot(self, data, style=None):
2707 if self.haslayout:
2708 raise RuntimeError("layout setup was already performed")
2709 if style is None:
2710 if helper.issequence(data):
2711 raise RuntimeError("list plot needs an explicit style")
2712 if self.defaultstyle.has_key(data.defaultstyle):
2713 style = self.defaultstyle[data.defaultstyle].iterate()
2714 else:
2715 style = data.defaultstyle()
2716 self.defaultstyle[data.defaultstyle] = style
2717 plotinfos = []
2718 first = 1
2719 for d in helper.ensuresequence(data):
2720 if not first:
2721 style = style.iterate()
2722 first = 0
2723 if d is not None:
2724 d.setstyle(self, style)
2725 plotinfos.append(plotinfo(d, style))
2726 self.plotinfos.extend(plotinfos)
2727 if helper.issequence(data):
2728 return plotinfos
2729 return plotinfos[0]
2731 def addkey(self, key, *plotinfos):
2732 if self.haslayout:
2733 raise RuntimeError("layout setup was already performed")
2734 self.addkeys.append((key, plotinfos))
2736 def _pos(self, x, y, xaxis=None, yaxis=None):
2737 if xaxis is None:
2738 xaxis = self.axes["x"]
2739 if yaxis is None:
2740 yaxis = self.axes["y"]
2741 return self._xpos+xaxis.convert(x)*self._width, self._ypos+yaxis.convert(y)*self._height
2743 def pos(self, x, y, xaxis=None, yaxis=None):
2744 if xaxis is None:
2745 xaxis = self.axes["x"]
2746 if yaxis is None:
2747 yaxis = self.axes["y"]
2748 return self.xpos+xaxis.convert(x)*self.width, self.ypos+yaxis.convert(y)*self.height
2750 def _vpos(self, vx, vy, xaxis=None, yaxis=None):
2751 if xaxis is None:
2752 xaxis = self.axes["x"]
2753 if yaxis is None:
2754 yaxis = self.axes["y"]
2755 return self._xpos+vx*self._width, self._ypos+vy*self._height
2757 def vpos(self, vx, vy, xaxis=None, yaxis=None):
2758 if xaxis is None:
2759 xaxis = self.axes["x"]
2760 if yaxis is None:
2761 yaxis = self.axes["y"]
2762 return self.xpos+vx*self.width, self.ypos+vy*self.height
2764 def xbaseline(self, x1=None, x2=None, axis=None):
2765 if axis is None:
2766 axis = self.axes["x"]
2767 if x1 is not None:
2768 v1 = axis.convert(x1)
2769 else:
2770 v1 = 0
2771 if x2 is not None:
2772 v2 = axis.convert(x2)
2773 else:
2774 v2 = 1
2775 return path._line(self._xpos+v1*self._width, axis._axispos,
2776 self._xpos+v2*self._width, axis._axispos)
2778 def ybaseline(self, y1=None, y2=None, axis=None):
2779 if axis is None:
2780 axis = self.axes["y"]
2781 if y1 is not None:
2782 v1 = axis.convert(y1)
2783 else:
2784 v1 = 0
2785 if y2 is not None:
2786 v2 = axis.convert(y2)
2787 else:
2788 v2 = 1
2789 return path._line(axis._axispos, self._ypos+v1*self._height,
2790 axis._axispos, self._ypos+v2*self._height)
2792 def vxbaseline(self, v1=None, v2=None, axis=None):
2793 if axis is None:
2794 axis = self.axes["x"]
2795 if v1 is None:
2796 v1 = 0
2797 if v2 is None:
2798 v2 = 1
2799 return path._line(self._xpos+v1*self._width, axis._axispos,
2800 self._xpos+v2*self._width, axis._axispos)
2802 def vybaseline(self, v1=None, v2=None, axis=None):
2803 if axis is None:
2804 axis = self.axes["y"]
2805 if v1 is None:
2806 v1 = 0
2807 if v2 is None:
2808 v2 = 1
2809 return path._line(axis._axispos, self._ypos+v1*self._height,
2810 axis._axispos, self._ypos+v2*self._height)
2812 def xgridline(self, x, axis=None):
2813 if axis is None:
2814 axis = self.axes["x"]
2815 v = axis.convert(x)
2816 return path._line(self._xpos+v*self._width, self._ypos,
2817 self._xpos+v*self._width, self._ypos+self._height)
2819 def ygridline(self, y, axis=None):
2820 if axis is None:
2821 axis = self.axes["y"]
2822 v = axis.convert(y)
2823 return path._line(self._xpos, self._ypos+v*self._height,
2824 self._xpos+self._width, self._ypos+v*self._height)
2826 def vxgridline(self, v, axis=None):
2827 if axis is None:
2828 axis = self.axes["x"]
2829 return path._line(self._xpos+v*self._width, self._ypos,
2830 self._xpos+v*self._width, self._ypos+self._height)
2832 def vygridline(self, v, axis=None):
2833 if axis is None:
2834 axis = self.axes["y"]
2835 return path._line(self._xpos, self._ypos+v*self._height,
2836 self._xpos+self._width, self._ypos+v*self._height)
2838 def _xtickpoint(self, x, axis=None):
2839 if axis is None:
2840 axis = self.axes["x"]
2841 return self._xpos+axis.convert(x)*self._width, axis._axispos
2843 def _ytickpoint(self, y, axis=None):
2844 if axis is None:
2845 axis = self.axes["y"]
2846 return axis._axispos, self._ypos+axis.convert(y)*self._height
2848 def xtickpoint(self, x, axis=None):
2849 if axis is None:
2850 axis = self.axes["x"]
2851 return self.xpos+axis.convert(x)*self.width, axis.axispos
2853 def ytickpoint(self, y, axis=None):
2854 if axis is None:
2855 axis = self.axes["y"]
2856 return axis.axispos, self.ypos+axis.convert(y)*self.height
2858 def _vxtickpoint(self, v, axis=None):
2859 if axis is None:
2860 axis = self.axes["x"]
2861 return self._xpos+v*self._width, axis._axispos
2863 def _vytickpoint(self, v, axis=None):
2864 if axis is None:
2865 axis = self.axes["y"]
2866 return axis._axispos, self._ypos+v*self._height
2868 def vxtickpoint(self, v, axis=None):
2869 if axis is None:
2870 axis = self.axes["x"]
2871 return self.xpos+v*self.width, axis.axispos
2873 def vytickpoint(self, v, axis=None):
2874 if axis is None:
2875 axis = self.axes["y"]
2876 return axis.axispos, self.ypos+v*self.height
2878 def xtickdirection(self, x, axis=None):
2879 if axis is None:
2880 axis = self.axes["x"]
2881 return axis.fixtickdirection
2883 def ytickdirection(self, y, axis=None):
2884 if axis is None:
2885 axis = self.axes["y"]
2886 return axis.fixtickdirection
2888 def vxtickdirection(self, v, axis=None):
2889 if axis is None:
2890 axis = self.axes["x"]
2891 return axis.fixtickdirection
2893 def vytickdirection(self, v, axis=None):
2894 if axis is None:
2895 axis = self.axes["y"]
2896 return axis.fixtickdirection
2898 def _addpos(self, x, y, dx, dy):
2899 return x+dx, y+dy
2901 def _connect(self, x1, y1, x2, y2):
2902 return path._lineto(x2, y2)
2904 def keynum(self, key):
2905 try:
2906 while key[0] in string.letters:
2907 key = key[1:]
2908 return int(key)
2909 except IndexError:
2910 return 1
2912 def gatherranges(self):
2913 ranges = {}
2914 for plotinfo in self.plotinfos:
2915 pdranges = plotinfo.data.getranges()
2916 if pdranges is not None:
2917 for key in pdranges.keys():
2918 if key not in ranges.keys():
2919 ranges[key] = pdranges[key]
2920 else:
2921 ranges[key] = (min(ranges[key][0], pdranges[key][0]),
2922 max(ranges[key][1], pdranges[key][1]))
2923 # known ranges are also set as ranges for the axes
2924 for key, axis in self.axes.items():
2925 if key in ranges.keys():
2926 axis.setdatarange(*ranges[key])
2927 ranges[key] = axis.getdatarange()
2928 if ranges[key] is None:
2929 del ranges[key]
2930 return ranges
2932 def removedomethod(self, method):
2933 hadmethod = 0
2934 while 1:
2935 try:
2936 self.domethods.remove(method)
2937 hadmethod = 1
2938 except ValueError:
2939 return hadmethod
2941 def dolayout(self):
2943 if not self.removedomethod(self.dolayout): return
2944 self.haslayout = 1
2945 # create list of ranges
2946 # 1. gather ranges
2947 ranges = self.gatherranges()
2948 # 2. calculate additional ranges out of known ranges
2949 for plotinfo in self.plotinfos:
2950 plotinfo.data.setranges(ranges)
2951 # 3. gather ranges again
2952 self.gatherranges()
2954 # do the layout for all axes
2955 axesdist = unit.length(self.axesdist_str, default_type="v")
2957 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
2958 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
2959 xaxisextents = [0, 0]
2960 yaxisextents = [0, 0]
2961 needxaxisdist = [0, 0]
2962 needyaxisdist = [0, 0]
2963 items = list(self.axes.items())
2964 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
2966 for key, axis in items:
2968 num = self.keynum(key)
2969 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
2970 num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
2972 if XPattern.match(key):
2974 if needxaxisdist[num2]:
2975 xaxisextents[num2] += axesdist
2977 axis.axispos = self.ypos + num2*self.height + num3*xaxisextents[num2]
2978 axis._axispos = unit.topt(axis.axispos)
2979 axis.fixtickdirection = (0, num3)
2981 axis.baseline = self.xbaseline
2982 axis.vbaseline = self.vxbaseline
2983 axis.gridline = self.xgridline
2984 axis.vgridline = self.vxgridline
2985 axis._tickpoint = self._xtickpoint
2986 axis.tickpoint = self.xtickpoint
2987 axis._vtickpoint = self._vxtickpoint
2988 axis.vtickpoint = self.vxtickpoint
2989 axis.tickdirection = self.xtickdirection
2990 axis.vtickdirection = self.vxtickdirection
2992 elif YPattern.match(key):
2994 if needyaxisdist[num2]:
2995 yaxisextents[num2] += axesdist
2997 axis.axispos = self.xpos + num2*self.width + num3*yaxisextents[num2]
2998 axis._axispos = unit.topt(axis.axispos)
2999 axis.fixtickdirection = (num3, 0)
3001 axis.baseline = self.ybaseline
3002 axis.vbaseline = self.vybaseline
3003 axis.gridline = self.ygridline
3004 axis.vgridline = self.vygridline
3005 axis._tickpoint = self._ytickpoint
3006 axis.tickpoint = self.ytickpoint
3007 axis._vtickpoint = self._vytickpoint
3008 axis.vtickpoint = self.vytickpoint
3009 axis.tickdirection = self.ytickdirection
3010 axis.vtickdirection = self.vytickdirection
3012 else:
3013 raise ValueError("Axis key '%s' not allowed" % key)
3015 axis.finish(self)
3017 if XPattern.match(key):
3018 xaxisextents[num2] += axis.axiscanvas.extent
3019 needxaxisdist[num2] = 1
3020 if YPattern.match(key):
3021 yaxisextents[num2] += axis.axiscanvas.extent
3022 needyaxisdist[num2] = 1
3024 def dobackground(self):
3025 self.dolayout()
3026 if not self.removedomethod(self.dobackground): return
3027 if self.backgroundattrs is not None:
3028 self.draw(path._rect(self._xpos, self._ypos, self._width, self._height),
3029 *helper.ensuresequence(self.backgroundattrs))
3031 def doaxes(self):
3032 self.dolayout()
3033 if not self.removedomethod(self.doaxes): return
3034 for axis in self.axes.values():
3035 self.insert(axis.axiscanvas)
3037 def dodata(self):
3038 self.dolayout()
3039 if not self.removedomethod(self.dodata): return
3040 for plotinfo in self.plotinfos:
3041 plotinfo.data.draw(self)
3043 def _dokey(self, key, *plotinfos):
3044 key.setplotinfos(*plotinfos)
3045 key.dolayout(self)
3046 bbox = key.bbox()
3047 if key.right:
3048 if key.hinside:
3049 x = self._xpos + self._width - bbox.urx - key._hdist
3050 else:
3051 x = self._xpos + self._width - bbox.llx + key._hdist
3052 else:
3053 if key.hinside:
3054 x = self._xpos - bbox.llx + key._hdist
3055 else:
3056 x = self._xpos - bbox.urx - key._hdist
3057 if key.top:
3058 if key.vinside:
3059 y = self._ypos + self._height - bbox.ury - key._vdist
3060 else:
3061 y = self._ypos + self._height - bbox.lly + key._vdist
3062 else:
3063 if key.vinside:
3064 y = self._ypos - bbox.lly + key._vdist
3065 else:
3066 y = self._ypos - bbox.ury - key._vdist
3067 key.paint(self, x, y)
3069 def dokey(self):
3070 self.dolayout()
3071 if not self.removedomethod(self.dokey): return
3072 if self.key is not None:
3073 self._dokey(self.key, *self.plotinfos)
3074 for key, plotinfos in self.addkeys:
3075 self._dokey(key, *plotinfos)
3077 def finish(self):
3078 while len(self.domethods):
3079 self.domethods[0]()
3081 def initwidthheight(self, width, height, ratio):
3082 if (width is not None) and (height is None):
3083 self.width = unit.length(width)
3084 self.height = (1.0/ratio) * self.width
3085 elif (height is not None) and (width is None):
3086 self.height = unit.length(height)
3087 self.width = ratio * self.height
3088 else:
3089 self.width = unit.length(width)
3090 self.height = unit.length(height)
3091 self._width = unit.topt(self.width)
3092 self._height = unit.topt(self.height)
3093 if self._width <= 0: raise ValueError("width <= 0")
3094 if self._height <= 0: raise ValueError("height <= 0")
3096 def initaxes(self, axes, addlinkaxes=0):
3097 for key in self.Names:
3098 if not axes.has_key(key):
3099 axes[key] = linaxis()
3100 elif axes[key] is None:
3101 del axes[key]
3102 if addlinkaxes:
3103 if not axes.has_key(key + "2") and axes.has_key(key):
3104 axes[key + "2"] = axes[key].createlinkaxis()
3105 elif axes[key + "2"] is None:
3106 del axes[key + "2"]
3107 self.axes = axes
3109 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
3110 key=None, backgroundattrs=None, axesdist="0.8 cm", **axes):
3111 canvas.canvas.__init__(self)
3112 self.xpos = unit.length(xpos)
3113 self.ypos = unit.length(ypos)
3114 self._xpos = unit.topt(self.xpos)
3115 self._ypos = unit.topt(self.ypos)
3116 self.initwidthheight(width, height, ratio)
3117 self.initaxes(axes, 1)
3118 self.key = key
3119 self.backgroundattrs = backgroundattrs
3120 self.axesdist_str = axesdist
3121 self.plotinfos = []
3122 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata, self.dokey]
3123 self.haslayout = 0
3124 self.defaultstyle = {}
3125 self.addkeys = []
3127 def bbox(self):
3128 self.finish()
3129 return canvas.canvas.bbox(self)
3131 def write(self, file):
3132 self.finish()
3133 canvas.canvas.write(self, file)
3137 # some thoughts, but deferred right now
3139 # class graphxyz(graphxy):
3141 # Names = "x", "y", "z"
3143 # def _vxtickpoint(self, axis, v):
3144 # return self._vpos(v, axis.vypos, axis.vzpos)
3146 # def _vytickpoint(self, axis, v):
3147 # return self._vpos(axis.vxpos, v, axis.vzpos)
3149 # def _vztickpoint(self, axis, v):
3150 # return self._vpos(axis.vxpos, axis.vypos, v)
3152 # def vxtickdirection(self, axis, v):
3153 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
3154 # x2, y2 = self._vpos(v, 0.5, 0)
3155 # dx, dy = x1 - x2, y1 - y2
3156 # norm = math.sqrt(dx*dx + dy*dy)
3157 # return dx/norm, dy/norm
3159 # def vytickdirection(self, axis, v):
3160 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
3161 # x2, y2 = self._vpos(0.5, v, 0)
3162 # dx, dy = x1 - x2, y1 - y2
3163 # norm = math.sqrt(dx*dx + dy*dy)
3164 # return dx/norm, dy/norm
3166 # def vztickdirection(self, axis, v):
3167 # return -1, 0
3168 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
3169 # x2, y2 = self._vpos(0.5, 0.5, v)
3170 # dx, dy = x1 - x2, y1 - y2
3171 # norm = math.sqrt(dx*dx + dy*dy)
3172 # return dx/norm, dy/norm
3174 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3175 # if xaxis is None: xaxis = self.axes["x"]
3176 # if yaxis is None: yaxis = self.axes["y"]
3177 # if zaxis is None: zaxis = self.axes["z"]
3178 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3180 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3181 # if xaxis is None: xaxis = self.axes["x"]
3182 # if yaxis is None: yaxis = self.axes["y"]
3183 # if zaxis is None: zaxis = self.axes["z"]
3184 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3186 # def _vpos(self, vx, vy, vz):
3187 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
3188 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
3189 # + self.a[2]*self.b[0]*(y-self.eye[1])
3190 # + self.a[1]*self.b[2]*(x-self.eye[0])
3191 # - self.a[2]*self.b[1]*(x-self.eye[0])
3192 # - self.a[0]*self.b[2]*(y-self.eye[1])
3193 # - self.a[1]*self.b[0]*(z-self.eye[2]))
3194 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
3195 # + self.eye[2]*self.b[0]*(y-self.eye[1])
3196 # + self.eye[1]*self.b[2]*(x-self.eye[0])
3197 # - self.eye[2]*self.b[1]*(x-self.eye[0])
3198 # - self.eye[0]*self.b[2]*(y-self.eye[1])
3199 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
3200 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
3201 # + self.a[2]*self.eye[0]*(y-self.eye[1])
3202 # + self.a[1]*self.eye[2]*(x-self.eye[0])
3203 # - self.a[2]*self.eye[1]*(x-self.eye[0])
3204 # - self.a[0]*self.eye[2]*(y-self.eye[1])
3205 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
3206 # return da/d0 + self._xpos, db/d0 + self._ypos
3208 # def vpos(self, vx, vy, vz):
3209 # tx, ty = self._vpos(vx, vy, vz)
3210 # return unit.t_pt(tx), unit.t_pt(ty)
3212 # def xbaseline(self, axis, x1, x2, xaxis=None):
3213 # if xaxis is None: xaxis = self.axes["x"]
3214 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2))
3216 # def ybaseline(self, axis, y1, y2, yaxis=None):
3217 # if yaxis is None: yaxis = self.axes["y"]
3218 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2))
3220 # def zbaseline(self, axis, z1, z2, zaxis=None):
3221 # if zaxis is None: zaxis = self.axes["z"]
3222 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2))
3224 # def vxbaseline(self, axis, v1, v2):
3225 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
3226 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
3227 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
3228 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
3230 # def vybaseline(self, axis, v1, v2):
3231 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
3232 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
3233 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
3234 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
3236 # def vzbaseline(self, axis, v1, v2):
3237 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
3238 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
3239 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
3240 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
3242 # def xgridpath(self, x, xaxis=None):
3243 # assert 0
3244 # if xaxis is None: xaxis = self.axes["x"]
3245 # v = xaxis.convert(x)
3246 # return path._line(self._xpos+v*self._width, self._ypos,
3247 # self._xpos+v*self._width, self._ypos+self._height)
3249 # def ygridpath(self, y, yaxis=None):
3250 # assert 0
3251 # if yaxis is None: yaxis = self.axes["y"]
3252 # v = yaxis.convert(y)
3253 # return path._line(self._xpos, self._ypos+v*self._height,
3254 # self._xpos+self._width, self._ypos+v*self._height)
3256 # def zgridpath(self, z, zaxis=None):
3257 # assert 0
3258 # if zaxis is None: zaxis = self.axes["z"]
3259 # v = zaxis.convert(z)
3260 # return path._line(self._xpos, self._zpos+v*self._height,
3261 # self._xpos+self._width, self._zpos+v*self._height)
3263 # def vxgridpath(self, v):
3264 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
3265 # path._lineto(*self._vpos(v, 0, 1)),
3266 # path._lineto(*self._vpos(v, 1, 1)),
3267 # path._lineto(*self._vpos(v, 1, 0)),
3268 # path.closepath())
3270 # def vygridpath(self, v):
3271 # return path.path(path._moveto(*self._vpos(0, v, 0)),
3272 # path._lineto(*self._vpos(0, v, 1)),
3273 # path._lineto(*self._vpos(1, v, 1)),
3274 # path._lineto(*self._vpos(1, v, 0)),
3275 # path.closepath())
3277 # def vzgridpath(self, v):
3278 # return path.path(path._moveto(*self._vpos(0, 0, v)),
3279 # path._lineto(*self._vpos(0, 1, v)),
3280 # path._lineto(*self._vpos(1, 1, v)),
3281 # path._lineto(*self._vpos(1, 0, v)),
3282 # path.closepath())
3284 # def _addpos(self, x, y, dx, dy):
3285 # assert 0
3286 # return x+dx, y+dy
3288 # def _connect(self, x1, y1, x2, y2):
3289 # assert 0
3290 # return path._lineto(x2, y2)
3292 # def doaxes(self):
3293 # self.dolayout()
3294 # if not self.removedomethod(self.doaxes): return
3295 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
3296 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3297 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3298 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[2])
3299 # items = list(self.axes.items())
3300 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3301 # for key, axis in items:
3302 # num = self.keynum(key)
3303 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3304 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3305 # if XPattern.match(key):
3306 # axis.vypos = 0
3307 # axis.vzpos = 0
3308 # axis._vtickpoint = self._vxtickpoint
3309 # axis.vgridpath = self.vxgridpath
3310 # axis.vbaseline = self.vxbaseline
3311 # axis.vtickdirection = self.vxtickdirection
3312 # elif YPattern.match(key):
3313 # axis.vxpos = 0
3314 # axis.vzpos = 0
3315 # axis._vtickpoint = self._vytickpoint
3316 # axis.vgridpath = self.vygridpath
3317 # axis.vbaseline = self.vybaseline
3318 # axis.vtickdirection = self.vytickdirection
3319 # elif ZPattern.match(key):
3320 # axis.vxpos = 0
3321 # axis.vypos = 0
3322 # axis._vtickpoint = self._vztickpoint
3323 # axis.vgridpath = self.vzgridpath
3324 # axis.vbaseline = self.vzbaseline
3325 # axis.vtickdirection = self.vztickdirection
3326 # else:
3327 # raise ValueError("Axis key '%s' not allowed" % key)
3328 # if axis.painter is not None:
3329 # axis.dopaint(self)
3330 # # if XPattern.match(key):
3331 # # self._xaxisextents[num2] += axis._extent
3332 # # needxaxisdist[num2] = 1
3333 # # if YPattern.match(key):
3334 # # self._yaxisextents[num2] += axis._extent
3335 # # needyaxisdist[num2] = 1
3337 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
3338 # phi=30, theta=30, distance=1,
3339 # backgroundattrs=None, axesdist="0.8 cm", **axes):
3340 # canvas.canvas.__init__(self)
3341 # self.tex = tex
3342 # self.xpos = xpos
3343 # self.ypos = ypos
3344 # self._xpos = unit.topt(xpos)
3345 # self._ypos = unit.topt(ypos)
3346 # self._width = unit.topt(width)
3347 # self._height = unit.topt(height)
3348 # self._depth = unit.topt(depth)
3349 # self.width = width
3350 # self.height = height
3351 # self.depth = depth
3352 # if self._width <= 0: raise ValueError("width < 0")
3353 # if self._height <= 0: raise ValueError("height < 0")
3354 # if self._depth <= 0: raise ValueError("height < 0")
3355 # self._distance = distance*math.sqrt(self._width*self._width+
3356 # self._height*self._height+
3357 # self._depth*self._depth)
3358 # phi *= -math.pi/180
3359 # theta *= math.pi/180
3360 # self.a = (-math.sin(phi), math.cos(phi), 0)
3361 # self.b = (-math.cos(phi)*math.sin(theta),
3362 # -math.sin(phi)*math.sin(theta),
3363 # math.cos(theta))
3364 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
3365 # self._distance*math.sin(phi)*math.cos(theta),
3366 # self._distance*math.sin(theta))
3367 # self.initaxes(axes)
3368 # self.axesdist_str = axesdist
3369 # self.backgroundattrs = backgroundattrs
3371 # self.data = []
3372 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
3373 # self.haslayout = 0
3374 # self.defaultstyle = {}
3376 # def bbox(self):
3377 # self.finish()
3378 # return bbox._bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
3381 ################################################################################
3382 # attr changers
3383 ################################################################################
3386 #class _Ichangeattr:
3387 # """attribute changer
3388 # is an iterator for attributes where an attribute
3389 # is not refered by just a number (like for a sequence),
3390 # but also by the number of attributes requested
3391 # by calls of the next method (like for an color palette)
3392 # (you should ensure to call all needed next before the attr)
3394 # the attribute itself is implemented by overloading the _attr method"""
3396 # def attr(self):
3397 # "get an attribute"
3399 # def next(self):
3400 # "get an attribute changer for the next attribute"
3403 class _changeattr: pass
3406 class changeattr(_changeattr):
3408 def __init__(self):
3409 self.counter = 1
3411 def getattr(self):
3412 return self.attr(0)
3414 def iterate(self):
3415 newindex = self.counter
3416 self.counter += 1
3417 return refattr(self, newindex)
3420 class refattr(_changeattr):
3422 def __init__(self, ref, index):
3423 self.ref = ref
3424 self.index = index
3426 def getattr(self):
3427 return self.ref.attr(self.index)
3429 def iterate(self):
3430 return self.ref.iterate()
3433 # helper routines for a using attrs
3435 def _getattr(attr):
3436 "get attr out of a attr/changeattr"
3437 if isinstance(attr, _changeattr):
3438 return attr.getattr()
3439 return attr
3442 def _getattrs(attrs):
3443 "get attrs out of a sequence of attr/changeattr"
3444 if attrs is not None:
3445 result = []
3446 for attr in helper.ensuresequence(attrs):
3447 if isinstance(attr, _changeattr):
3448 attr = attr.getattr()
3449 if attr is not None:
3450 result.append(attr)
3451 if len(result) or not len(attrs):
3452 return result
3455 def _iterateattr(attr):
3456 "perform next to a attr/changeattr"
3457 if isinstance(attr, _changeattr):
3458 return attr.iterate()
3459 return attr
3462 def _iterateattrs(attrs):
3463 "perform next to a sequence of attr/changeattr"
3464 if attrs is not None:
3465 result = []
3466 for attr in helper.ensuresequence(attrs):
3467 if isinstance(attr, _changeattr):
3468 result.append(attr.iterate())
3469 else:
3470 result.append(attr)
3471 return result
3474 class changecolor(changeattr):
3476 def __init__(self, palette):
3477 changeattr.__init__(self)
3478 self.palette = palette
3480 def attr(self, index):
3481 if self.counter != 1:
3482 return self.palette.getcolor(index/float(self.counter-1))
3483 else:
3484 return self.palette.getcolor(0)
3487 class _changecolorgray(changecolor):
3489 def __init__(self, palette=color.palette.Gray):
3490 changecolor.__init__(self, palette)
3492 _changecolorgrey = _changecolorgray
3495 class _changecolorreversegray(changecolor):
3497 def __init__(self, palette=color.palette.ReverseGray):
3498 changecolor.__init__(self, palette)
3500 _changecolorreversegrey = _changecolorreversegray
3503 class _changecolorredblack(changecolor):
3505 def __init__(self, palette=color.palette.RedBlack):
3506 changecolor.__init__(self, palette)
3509 class _changecolorblackred(changecolor):
3511 def __init__(self, palette=color.palette.BlackRed):
3512 changecolor.__init__(self, palette)
3515 class _changecolorredwhite(changecolor):
3517 def __init__(self, palette=color.palette.RedWhite):
3518 changecolor.__init__(self, palette)
3521 class _changecolorwhitered(changecolor):
3523 def __init__(self, palette=color.palette.WhiteRed):
3524 changecolor.__init__(self, palette)
3527 class _changecolorgreenblack(changecolor):
3529 def __init__(self, palette=color.palette.GreenBlack):
3530 changecolor.__init__(self, palette)
3533 class _changecolorblackgreen(changecolor):
3535 def __init__(self, palette=color.palette.BlackGreen):
3536 changecolor.__init__(self, palette)
3539 class _changecolorgreenwhite(changecolor):
3541 def __init__(self, palette=color.palette.GreenWhite):
3542 changecolor.__init__(self, palette)
3545 class _changecolorwhitegreen(changecolor):
3547 def __init__(self, palette=color.palette.WhiteGreen):
3548 changecolor.__init__(self, palette)
3551 class _changecolorblueblack(changecolor):
3553 def __init__(self, palette=color.palette.BlueBlack):
3554 changecolor.__init__(self, palette)
3557 class _changecolorblackblue(changecolor):
3559 def __init__(self, palette=color.palette.BlackBlue):
3560 changecolor.__init__(self, palette)
3563 class _changecolorbluewhite(changecolor):
3565 def __init__(self, palette=color.palette.BlueWhite):
3566 changecolor.__init__(self, palette)
3569 class _changecolorwhiteblue(changecolor):
3571 def __init__(self, palette=color.palette.WhiteBlue):
3572 changecolor.__init__(self, palette)
3575 class _changecolorredgreen(changecolor):
3577 def __init__(self, palette=color.palette.RedGreen):
3578 changecolor.__init__(self, palette)
3581 class _changecolorredblue(changecolor):
3583 def __init__(self, palette=color.palette.RedBlue):
3584 changecolor.__init__(self, palette)
3587 class _changecolorgreenred(changecolor):
3589 def __init__(self, palette=color.palette.GreenRed):
3590 changecolor.__init__(self, palette)
3593 class _changecolorgreenblue(changecolor):
3595 def __init__(self, palette=color.palette.GreenBlue):
3596 changecolor.__init__(self, palette)
3599 class _changecolorbluered(changecolor):
3601 def __init__(self, palette=color.palette.BlueRed):
3602 changecolor.__init__(self, palette)
3605 class _changecolorbluegreen(changecolor):
3607 def __init__(self, palette=color.palette.BlueGreen):
3608 changecolor.__init__(self, palette)
3611 class _changecolorrainbow(changecolor):
3613 def __init__(self, palette=color.palette.Rainbow):
3614 changecolor.__init__(self, palette)
3617 class _changecolorreverserainbow(changecolor):
3619 def __init__(self, palette=color.palette.ReverseRainbow):
3620 changecolor.__init__(self, palette)
3623 class _changecolorhue(changecolor):
3625 def __init__(self, palette=color.palette.Hue):
3626 changecolor.__init__(self, palette)
3629 class _changecolorreversehue(changecolor):
3631 def __init__(self, palette=color.palette.ReverseHue):
3632 changecolor.__init__(self, palette)
3635 changecolor.Gray = _changecolorgray
3636 changecolor.Grey = _changecolorgrey
3637 changecolor.Reversegray = _changecolorreversegray
3638 changecolor.Reversegrey = _changecolorreversegrey
3639 changecolor.RedBlack = _changecolorredblack
3640 changecolor.BlackRed = _changecolorblackred
3641 changecolor.RedWhite = _changecolorredwhite
3642 changecolor.WhiteRed = _changecolorwhitered
3643 changecolor.GreenBlack = _changecolorgreenblack
3644 changecolor.BlackGreen = _changecolorblackgreen
3645 changecolor.GreenWhite = _changecolorgreenwhite
3646 changecolor.WhiteGreen = _changecolorwhitegreen
3647 changecolor.BlueBlack = _changecolorblueblack
3648 changecolor.BlackBlue = _changecolorblackblue
3649 changecolor.BlueWhite = _changecolorbluewhite
3650 changecolor.WhiteBlue = _changecolorwhiteblue
3651 changecolor.RedGreen = _changecolorredgreen
3652 changecolor.RedBlue = _changecolorredblue
3653 changecolor.GreenRed = _changecolorgreenred
3654 changecolor.GreenBlue = _changecolorgreenblue
3655 changecolor.BlueRed = _changecolorbluered
3656 changecolor.BlueGreen = _changecolorbluegreen
3657 changecolor.Rainbow = _changecolorrainbow
3658 changecolor.ReverseRainbow = _changecolorreverserainbow
3659 changecolor.Hue = _changecolorhue
3660 changecolor.ReverseHue = _changecolorreversehue
3663 class changesequence(changeattr):
3664 "cycles through a sequence"
3666 def __init__(self, *sequence):
3667 changeattr.__init__(self)
3668 if not len(sequence):
3669 sequence = self.defaultsequence
3670 self.sequence = sequence
3672 def attr(self, index):
3673 return self.sequence[index % len(self.sequence)]
3676 class changelinestyle(changesequence):
3677 defaultsequence = (canvas.linestyle.solid,
3678 canvas.linestyle.dashed,
3679 canvas.linestyle.dotted,
3680 canvas.linestyle.dashdotted)
3683 class changestrokedfilled(changesequence):
3684 defaultsequence = (canvas.stroked(), canvas.filled())
3687 class changefilledstroked(changesequence):
3688 defaultsequence = (canvas.filled(), canvas.stroked())
3692 ################################################################################
3693 # styles
3694 ################################################################################
3697 class symbol:
3699 def cross(self, x, y):
3700 return (path._moveto(x-0.5*self._size, y-0.5*self._size),
3701 path._lineto(x+0.5*self._size, y+0.5*self._size),
3702 path._moveto(x-0.5*self._size, y+0.5*self._size),
3703 path._lineto(x+0.5*self._size, y-0.5*self._size))
3705 def plus(self, x, y):
3706 return (path._moveto(x-0.707106781*self._size, y),
3707 path._lineto(x+0.707106781*self._size, y),
3708 path._moveto(x, y-0.707106781*self._size),
3709 path._lineto(x, y+0.707106781*self._size))
3711 def square(self, x, y):
3712 return (path._moveto(x-0.5*self._size, y-0.5 * self._size),
3713 path._lineto(x+0.5*self._size, y-0.5 * self._size),
3714 path._lineto(x+0.5*self._size, y+0.5 * self._size),
3715 path._lineto(x-0.5*self._size, y+0.5 * self._size),
3716 path.closepath())
3718 def triangle(self, x, y):
3719 return (path._moveto(x-0.759835685*self._size, y-0.438691337*self._size),
3720 path._lineto(x+0.759835685*self._size, y-0.438691337*self._size),
3721 path._lineto(x, y+0.877382675*self._size),
3722 path.closepath())
3724 def circle(self, x, y):
3725 return (path._arc(x, y, 0.564189583*self._size, 0, 360),
3726 path.closepath())
3728 def diamond(self, x, y):
3729 return (path._moveto(x-0.537284965*self._size, y),
3730 path._lineto(x, y-0.930604859*self._size),
3731 path._lineto(x+0.537284965*self._size, y),
3732 path._lineto(x, y+0.930604859*self._size),
3733 path.closepath())
3735 def __init__(self, symbol=helper.nodefault,
3736 size="0.2 cm", symbolattrs=canvas.stroked(),
3737 errorscale=0.5, errorbarattrs=(),
3738 lineattrs=None):
3739 self.size_str = size
3740 if symbol is helper.nodefault:
3741 self._symbol = changesymbol.cross()
3742 else:
3743 self._symbol = symbol
3744 self._symbolattrs = symbolattrs
3745 self.errorscale = errorscale
3746 self._errorbarattrs = errorbarattrs
3747 self._lineattrs = lineattrs
3749 def iteratedict(self):
3750 result = {}
3751 result["symbol"] = _iterateattr(self._symbol)
3752 result["size"] = _iterateattr(self.size_str)
3753 result["symbolattrs"] = _iterateattrs(self._symbolattrs)
3754 result["errorscale"] = _iterateattr(self.errorscale)
3755 result["errorbarattrs"] = _iterateattrs(self._errorbarattrs)
3756 result["lineattrs"] = _iterateattrs(self._lineattrs)
3757 return result
3759 def iterate(self):
3760 return symbol(**self.iteratedict())
3762 def othercolumnkey(self, key, index):
3763 raise ValueError("unsuitable key '%s'" % key)
3765 def setcolumns(self, graph, columns):
3766 def checkpattern(key, index, pattern, iskey, isindex):
3767 if key is not None:
3768 match = pattern.match(key)
3769 if match:
3770 if isindex is not None: raise ValueError("multiple key specification")
3771 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
3772 key = None
3773 iskey = match.groups()[0]
3774 isindex = index
3775 return key, iskey, isindex
3777 self.xi = self.xmini = self.xmaxi = None
3778 self.dxi = self.dxmini = self.dxmaxi = None
3779 self.yi = self.ymini = self.ymaxi = None
3780 self.dyi = self.dymini = self.dymaxi = None
3781 self.xkey = self.ykey = None
3782 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
3783 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3784 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3785 XMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
3786 YMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
3787 XMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
3788 YMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
3789 DXPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3790 DYPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3791 DXMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
3792 DYMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
3793 DXMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
3794 DYMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
3795 for key, index in columns.items():
3796 key, self.xkey, self.xi = checkpattern(key, index, XPattern, self.xkey, self.xi)
3797 key, self.ykey, self.yi = checkpattern(key, index, YPattern, self.ykey, self.yi)
3798 key, self.xkey, self.xmini = checkpattern(key, index, XMinPattern, self.xkey, self.xmini)
3799 key, self.ykey, self.ymini = checkpattern(key, index, YMinPattern, self.ykey, self.ymini)
3800 key, self.xkey, self.xmaxi = checkpattern(key, index, XMaxPattern, self.xkey, self.xmaxi)
3801 key, self.ykey, self.ymaxi = checkpattern(key, index, YMaxPattern, self.ykey, self.ymaxi)
3802 key, self.xkey, self.dxi = checkpattern(key, index, DXPattern, self.xkey, self.dxi)
3803 key, self.ykey, self.dyi = checkpattern(key, index, DYPattern, self.ykey, self.dyi)
3804 key, self.xkey, self.dxmini = checkpattern(key, index, DXMinPattern, self.xkey, self.dxmini)
3805 key, self.ykey, self.dymini = checkpattern(key, index, DYMinPattern, self.ykey, self.dymini)
3806 key, self.xkey, self.dxmaxi = checkpattern(key, index, DXMaxPattern, self.xkey, self.dxmaxi)
3807 key, self.ykey, self.dymaxi = checkpattern(key, index, DYMaxPattern, self.ykey, self.dymaxi)
3808 if key is not None:
3809 self.othercolumnkey(key, index)
3810 if None in (self.xkey, self.ykey): raise ValueError("incomplete axis specification")
3811 if (len(filter(None, (self.xmini, self.dxmini, self.dxi))) > 1 or
3812 len(filter(None, (self.ymini, self.dymini, self.dyi))) > 1 or
3813 len(filter(None, (self.xmaxi, self.dxmaxi, self.dxi))) > 1 or
3814 len(filter(None, (self.ymaxi, self.dymaxi, self.dyi))) > 1):
3815 raise ValueError("multiple errorbar definition")
3816 if ((self.xi is None and self.dxi is not None) or
3817 (self.yi is None and self.dyi is not None) or
3818 (self.xi is None and self.dxmini is not None) or
3819 (self.yi is None and self.dymini is not None) or
3820 (self.xi is None and self.dxmaxi is not None) or
3821 (self.yi is None and self.dymaxi is not None)):
3822 raise ValueError("errorbar definition start value missing")
3823 self.xaxis = graph.axes[self.xkey]
3824 self.yaxis = graph.axes[self.ykey]
3826 def minmidmax(self, point, i, mini, maxi, di, dmini, dmaxi):
3827 min = max = mid = None
3828 try:
3829 mid = point[i] + 0.0
3830 except (TypeError, ValueError):
3831 pass
3832 try:
3833 if di is not None: min = point[i] - point[di]
3834 elif dmini is not None: min = point[i] - point[dmini]
3835 elif mini is not None: min = point[mini] + 0.0
3836 except (TypeError, ValueError):
3837 pass
3838 try:
3839 if di is not None: max = point[i] + point[di]
3840 elif dmaxi is not None: max = point[i] + point[dmaxi]
3841 elif maxi is not None: max = point[maxi] + 0.0
3842 except (TypeError, ValueError):
3843 pass
3844 if mid is not None:
3845 if min is not None and min > mid: raise ValueError("minimum error in errorbar")
3846 if max is not None and max < mid: raise ValueError("maximum error in errorbar")
3847 else:
3848 if min is not None and max is not None and min > max: raise ValueError("minimum/maximum error in errorbar")
3849 return min, mid, max
3851 def keyrange(self, points, i, mini, maxi, di, dmini, dmaxi):
3852 allmin = allmax = None
3853 if filter(None, (mini, maxi, di, dmini, dmaxi)) is not None:
3854 for point in points:
3855 min, mid, max = self.minmidmax(point, i, mini, maxi, di, dmini, dmaxi)
3856 if min is not None and (allmin is None or min < allmin): allmin = min
3857 if mid is not None and (allmin is None or mid < allmin): allmin = mid
3858 if mid is not None and (allmax is None or mid > allmax): allmax = mid
3859 if max is not None and (allmax is None or max > allmax): allmax = max
3860 else:
3861 for point in points:
3862 try:
3863 value = point[i] + 0.0
3864 if allmin is None or point[i] < allmin: allmin = point[i]
3865 if allmax is None or point[i] > allmax: allmax = point[i]
3866 except (TypeError, ValueError):
3867 pass
3868 return allmin, allmax
3870 def getranges(self, points):
3871 xmin, xmax = self.keyrange(points, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
3872 ymin, ymax = self.keyrange(points, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
3873 return {self.xkey: (xmin, xmax), self.ykey: (ymin, ymax)}
3875 def _drawerrorbar(self, graph, topleft, top, topright,
3876 left, center, right,
3877 bottomleft, bottom, bottomright, point=None):
3878 if left is not None:
3879 if right is not None:
3880 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3881 left2 = graph._addpos(*(left+(0, self._errorsize)))
3882 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3883 right2 = graph._addpos(*(right+(0, self._errorsize)))
3884 graph.stroke(path.path(path._moveto(*left1),
3885 graph._connect(*(left1+left2)),
3886 path._moveto(*left),
3887 graph._connect(*(left+right)),
3888 path._moveto(*right1),
3889 graph._connect(*(right1+right2))),
3890 *self.errorbarattrs)
3891 elif center is not None:
3892 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3893 left2 = graph._addpos(*(left+(0, self._errorsize)))
3894 graph.stroke(path.path(path._moveto(*left1),
3895 graph._connect(*(left1+left2)),
3896 path._moveto(*left),
3897 graph._connect(*(left+center))),
3898 *self.errorbarattrs)
3899 else:
3900 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3901 left2 = graph._addpos(*(left+(0, self._errorsize)))
3902 left3 = graph._addpos(*(left+(self._errorsize, 0)))
3903 graph.stroke(path.path(path._moveto(*left1),
3904 graph._connect(*(left1+left2)),
3905 path._moveto(*left),
3906 graph._connect(*(left+left3))),
3907 *self.errorbarattrs)
3908 if right is not None and left is None:
3909 if center is not None:
3910 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3911 right2 = graph._addpos(*(right+(0, self._errorsize)))
3912 graph.stroke(path.path(path._moveto(*right1),
3913 graph._connect(*(right1+right2)),
3914 path._moveto(*right),
3915 graph._connect(*(right+center))),
3916 *self.errorbarattrs)
3917 else:
3918 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3919 right2 = graph._addpos(*(right+(0, self._errorsize)))
3920 right3 = graph._addpos(*(right+(-self._errorsize, 0)))
3921 graph.stroke(path.path(path._moveto(*right1),
3922 graph._connect(*(right1+right2)),
3923 path._moveto(*right),
3924 graph._connect(*(right+right3))),
3925 *self.errorbarattrs)
3927 if bottom is not None:
3928 if top is not None:
3929 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3930 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3931 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3932 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3933 graph.stroke(path.path(path._moveto(*bottom1),
3934 graph._connect(*(bottom1+bottom2)),
3935 path._moveto(*bottom),
3936 graph._connect(*(bottom+top)),
3937 path._moveto(*top1),
3938 graph._connect(*(top1+top2))),
3939 *self.errorbarattrs)
3940 elif center is not None:
3941 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3942 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3943 graph.stroke(path.path(path._moveto(*bottom1),
3944 graph._connect(*(bottom1+bottom2)),
3945 path._moveto(*bottom),
3946 graph._connect(*(bottom+center))),
3947 *self.errorbarattrs)
3948 else:
3949 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3950 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3951 bottom3 = graph._addpos(*(bottom+(0, self._errorsize)))
3952 graph.stroke(path.path(path._moveto(*bottom1),
3953 graph._connect(*(bottom1+bottom2)),
3954 path._moveto(*bottom),
3955 graph._connect(*(bottom+bottom3))),
3956 *self.errorbarattrs)
3957 if top is not None and bottom is None:
3958 if center is not None:
3959 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3960 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3961 graph.stroke(path.path(path._moveto(*top1),
3962 graph._connect(*(top1+top2)),
3963 path._moveto(*top),
3964 graph._connect(*(top+center))),
3965 *self.errorbarattrs)
3966 else:
3967 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3968 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3969 top3 = graph._addpos(*(top+(0, -self._errorsize)))
3970 graph.stroke(path.path(path._moveto(*top1),
3971 graph._connect(*(top1+top2)),
3972 path._moveto(*top),
3973 graph._connect(*(top+top3))),
3974 *self.errorbarattrs)
3975 if bottomleft is not None:
3976 if topleft is not None and bottomright is None:
3977 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
3978 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
3979 graph.stroke(path.path(path._moveto(*bottomleft1),
3980 graph._connect(*(bottomleft1+bottomleft)),
3981 graph._connect(*(bottomleft+topleft)),
3982 graph._connect(*(topleft+topleft1))),
3983 *self.errorbarattrs)
3984 elif bottomright is not None and topleft is None:
3985 bottomleft1 = graph._addpos(*(bottomleft+(0, self._errorsize)))
3986 bottomright1 = graph._addpos(*(bottomright+(0, self._errorsize)))
3987 graph.stroke(path.path(path._moveto(*bottomleft1),
3988 graph._connect(*(bottomleft1+bottomleft)),
3989 graph._connect(*(bottomleft+bottomright)),
3990 graph._connect(*(bottomright+bottomright1))),
3991 *self.errorbarattrs)
3992 elif bottomright is None and topleft is None:
3993 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
3994 bottomleft2 = graph._addpos(*(bottomleft+(0, self._errorsize)))
3995 graph.stroke(path.path(path._moveto(*bottomleft1),
3996 graph._connect(*(bottomleft1+bottomleft)),
3997 graph._connect(*(bottomleft+bottomleft2))),
3998 *self.errorbarattrs)
3999 if topright is not None:
4000 if bottomright is not None and topleft is None:
4001 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
4002 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
4003 graph.stroke(path.path(path._moveto(*topright1),
4004 graph._connect(*(topright1+topright)),
4005 graph._connect(*(topright+bottomright)),
4006 graph._connect(*(bottomright+bottomright1))),
4007 *self.errorbarattrs)
4008 elif topleft is not None and bottomright is None:
4009 topright1 = graph._addpos(*(topright+(0, -self._errorsize)))
4010 topleft1 = graph._addpos(*(topleft+(0, -self._errorsize)))
4011 graph.stroke(path.path(path._moveto(*topright1),
4012 graph._connect(*(topright1+topright)),
4013 graph._connect(*(topright+topleft)),
4014 graph._connect(*(topleft+topleft1))),
4015 *self.errorbarattrs)
4016 elif topleft is None and bottomright is None:
4017 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
4018 topright2 = graph._addpos(*(topright+(0, -self._errorsize)))
4019 graph.stroke(path.path(path._moveto(*topright1),
4020 graph._connect(*(topright1+topright)),
4021 graph._connect(*(topright+topright2))),
4022 *self.errorbarattrs)
4023 if bottomright is not None and bottomleft is None and topright is None:
4024 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
4025 bottomright2 = graph._addpos(*(bottomright+(0, self._errorsize)))
4026 graph.stroke(path.path(path._moveto(*bottomright1),
4027 graph._connect(*(bottomright1+bottomright)),
4028 graph._connect(*(bottomright+bottomright2))),
4029 *self.errorbarattrs)
4030 if topleft is not None and bottomleft is None and topright is None:
4031 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
4032 topleft2 = graph._addpos(*(topleft+(0, -self._errorsize)))
4033 graph.stroke(path.path(path._moveto(*topleft1),
4034 graph._connect(*(topleft1+topleft)),
4035 graph._connect(*(topleft+topleft2))),
4036 *self.errorbarattrs)
4037 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4038 graph.stroke(path.path(path._moveto(*bottomleft),
4039 graph._connect(*(bottomleft+bottomright)),
4040 graph._connect(*(bottomright+topright)),
4041 graph._connect(*(topright+topleft)),
4042 path.closepath()),
4043 *self.errorbarattrs)
4045 def _drawsymbol(self, canvas, x, y, point=None):
4046 canvas.draw(path.path(*self.symbol(self, x, y)), *self.symbolattrs)
4048 def drawsymbol(self, canvas, x, y, point=None):
4049 self._drawsymbol(canvas, unit.topt(x), unit.topt(y), point)
4051 def key(self, c, x, y, width, height):
4052 if self._symbolattrs is not None:
4053 self._drawsymbol(c, x + 0.5 * width, y + 0.5 * height)
4054 if self._lineattrs is not None:
4055 c.stroke(path._line(x, y + 0.5 * height, x + width, y + 0.5 * height), *self.lineattrs)
4057 def drawpoints(self, graph, points):
4058 xaxismin, xaxismax = self.xaxis.getdatarange()
4059 yaxismin, yaxismax = self.yaxis.getdatarange()
4060 self.size = unit.length(_getattr(self.size_str), default_type="v")
4061 self._size = unit.topt(self.size)
4062 self.symbol = _getattr(self._symbol)
4063 self.symbolattrs = _getattrs(helper.ensuresequence(self._symbolattrs))
4064 self.errorbarattrs = _getattrs(helper.ensuresequence(self._errorbarattrs))
4065 self._errorsize = self.errorscale * self._size
4066 self.errorsize = self.errorscale * self.size
4067 self.lineattrs = _getattrs(helper.ensuresequence(self._lineattrs))
4068 if self._lineattrs is not None:
4069 clipcanvas = graph.clipcanvas()
4070 lineels = []
4071 haserror = filter(None, (self.xmini, self.ymini, self.xmaxi, self.ymaxi,
4072 self.dxi, self.dyi, self.dxmini, self.dymini, self.dxmaxi, self.dymaxi)) is not None
4073 moveto = 1
4074 for point in points:
4075 drawsymbol = 1
4076 xmin, x, xmax = self.minmidmax(point, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4077 ymin, y, ymax = self.minmidmax(point, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4078 if x is not None and x < xaxismin: drawsymbol = 0
4079 elif x is not None and x > xaxismax: drawsymbol = 0
4080 elif y is not None and y < yaxismin: drawsymbol = 0
4081 elif y is not None and y > yaxismax: drawsymbol = 0
4082 elif haserror:
4083 if xmin is not None and xmin < xaxismin: drawsymbol = 0
4084 elif xmax is not None and xmax < xaxismin: drawsymbol = 0
4085 elif xmax is not None and xmax > xaxismax: drawsymbol = 0
4086 elif xmin is not None and xmin > xaxismax: drawsymbol = 0
4087 elif ymin is not None and ymin < yaxismin: drawsymbol = 0
4088 elif ymax is not None and ymax < yaxismin: drawsymbol = 0
4089 elif ymax is not None and ymax > yaxismax: drawsymbol = 0
4090 elif ymin is not None and ymin > yaxismax: drawsymbol = 0
4091 xpos=ypos=topleft=top=topright=left=center=right=bottomleft=bottom=bottomright=None
4092 if x is not None and y is not None:
4093 try:
4094 center = xpos, ypos = graph._pos(x, y, xaxis=self.xaxis, yaxis=self.yaxis)
4095 except (ValueError, OverflowError): # XXX: exceptions???
4096 pass
4097 if haserror:
4098 if y is not None:
4099 if xmin is not None: left = graph._pos(xmin, y, xaxis=self.xaxis, yaxis=self.yaxis)
4100 if xmax is not None: right = graph._pos(xmax, y, xaxis=self.xaxis, yaxis=self.yaxis)
4101 if x is not None:
4102 if ymax is not None: top = graph._pos(x, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4103 if ymin is not None: bottom = graph._pos(x, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4104 if x is None or y is None:
4105 if ymax is not None:
4106 if xmin is not None: topleft = graph._pos(xmin, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4107 if xmax is not None: topright = graph._pos(xmax, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4108 if ymin is not None:
4109 if xmin is not None: bottomleft = graph._pos(xmin, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4110 if xmax is not None: bottomright = graph._pos(xmax, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4111 if drawsymbol:
4112 if self._errorbarattrs is not None and haserror:
4113 self._drawerrorbar(graph, topleft, top, topright,
4114 left, center, right,
4115 bottomleft, bottom, bottomright, point)
4116 if self._symbolattrs is not None and xpos is not None and ypos is not None:
4117 self._drawsymbol(graph, xpos, ypos, point)
4118 if xpos is not None and ypos is not None:
4119 if moveto:
4120 lineels.append(path._moveto(xpos, ypos))
4121 moveto = 0
4122 else:
4123 lineels.append(path._lineto(xpos, ypos))
4124 else:
4125 moveto = 1
4126 self.path = path.path(*lineels)
4127 if self._lineattrs is not None:
4128 clipcanvas.stroke(self.path, *self.lineattrs)
4131 class changesymbol(changesequence): pass
4134 class _changesymbolcross(changesymbol):
4135 defaultsequence = (symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond)
4138 class _changesymbolplus(changesymbol):
4139 defaultsequence = (symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross)
4142 class _changesymbolsquare(changesymbol):
4143 defaultsequence = (symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus)
4146 class _changesymboltriangle(changesymbol):
4147 defaultsequence = (symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square)
4150 class _changesymbolcircle(changesymbol):
4151 defaultsequence = (symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle)
4154 class _changesymboldiamond(changesymbol):
4155 defaultsequence = (symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle)
4158 class _changesymbolsquaretwice(changesymbol):
4159 defaultsequence = (symbol.square, symbol.square, symbol.triangle, symbol.triangle,
4160 symbol.circle, symbol.circle, symbol.diamond, symbol.diamond)
4163 class _changesymboltriangletwice(changesymbol):
4164 defaultsequence = (symbol.triangle, symbol.triangle, symbol.circle, symbol.circle,
4165 symbol.diamond, symbol.diamond, symbol.square, symbol.square)
4168 class _changesymbolcircletwice(changesymbol):
4169 defaultsequence = (symbol.circle, symbol.circle, symbol.diamond, symbol.diamond,
4170 symbol.square, symbol.square, symbol.triangle, symbol.triangle)
4173 class _changesymboldiamondtwice(changesymbol):
4174 defaultsequence = (symbol.diamond, symbol.diamond, symbol.square, symbol.square,
4175 symbol.triangle, symbol.triangle, symbol.circle, symbol.circle)
4178 changesymbol.cross = _changesymbolcross
4179 changesymbol.plus = _changesymbolplus
4180 changesymbol.square = _changesymbolsquare
4181 changesymbol.triangle = _changesymboltriangle
4182 changesymbol.circle = _changesymbolcircle
4183 changesymbol.diamond = _changesymboldiamond
4184 changesymbol.squaretwice = _changesymbolsquaretwice
4185 changesymbol.triangletwice = _changesymboltriangletwice
4186 changesymbol.circletwice = _changesymbolcircletwice
4187 changesymbol.diamondtwice = _changesymboldiamondtwice
4190 class line(symbol):
4192 def __init__(self, lineattrs=helper.nodefault):
4193 if lineattrs is helper.nodefault:
4194 lineattrs = (changelinestyle(), canvas.linejoin.round)
4195 symbol.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
4198 class rect(symbol):
4200 def __init__(self, palette=color.palette.Gray):
4201 self.palette = palette
4202 self.colorindex = None
4203 symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
4205 def iterate(self):
4206 raise RuntimeError("style is not iterateable")
4208 def othercolumnkey(self, key, index):
4209 if key == "color":
4210 self.colorindex = index
4211 else:
4212 symbol.othercolumnkey(self, key, index)
4214 def _drawerrorbar(self, graph, topleft, top, topright,
4215 left, center, right,
4216 bottomleft, bottom, bottomright, point=None):
4217 color = point[self.colorindex]
4218 if color is not None:
4219 if color != self.lastcolor:
4220 self.rectclipcanvas.set(self.palette.getcolor(color))
4221 if bottom is not None and left is not None:
4222 bottomleft = left[0], bottom[1]
4223 if bottom is not None and right is not None:
4224 bottomright = right[0], bottom[1]
4225 if top is not None and right is not None:
4226 topright = right[0], top[1]
4227 if top is not None and left is not None:
4228 topleft = left[0], top[1]
4229 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4230 self.rectclipcanvas.fill(path.path(path._moveto(*bottomleft),
4231 graph._connect(*(bottomleft+bottomright)),
4232 graph._connect(*(bottomright+topright)),
4233 graph._connect(*(topright+topleft)),
4234 path.closepath()))
4236 def drawpoints(self, graph, points):
4237 if self.colorindex is None:
4238 raise RuntimeError("column 'color' not set")
4239 self.lastcolor = None
4240 self.rectclipcanvas = graph.clipcanvas()
4241 symbol.drawpoints(self, graph, points)
4243 def key(self, c, x, y, width, height):
4244 raise RuntimeError("style doesn't yet provide a key")
4247 class text(symbol):
4249 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=textmodule.halign.center, **args):
4250 self.textindex = None
4251 self.textdx_str = textdx
4252 self.textdy_str = textdy
4253 self._textattrs = textattrs
4254 symbol.__init__(self, **args)
4256 def iteratedict(self):
4257 result = symbol.iteratedict()
4258 result["textattrs"] = _iterateattr(self._textattrs)
4259 return result
4261 def iterate(self):
4262 return textsymbol(**self.iteratedict())
4264 def othercolumnkey(self, key, index):
4265 if key == "text":
4266 self.textindex = index
4267 else:
4268 symbol.othercolumnkey(self, key, index)
4270 def _drawsymbol(self, graph, x, y, point=None):
4271 symbol._drawsymbol(self, graph, x, y, point)
4272 if None not in (x, y, point[self.textindex]) and self._textattrs is not None:
4273 graph._text(x + self._textdx, y + self._textdy, str(point[self.textindex]), *helper.ensuresequence(self.textattrs))
4275 def drawpoints(self, graph, points):
4276 self.textdx = unit.length(_getattr(self.textdx_str), default_type="v")
4277 self.textdy = unit.length(_getattr(self.textdy_str), default_type="v")
4278 self._textdx = unit.topt(self.textdx)
4279 self._textdy = unit.topt(self.textdy)
4280 if self._textattrs is not None:
4281 self.textattrs = _getattr(self._textattrs)
4282 if self.textindex is None:
4283 raise RuntimeError("column 'text' not set")
4284 symbol.drawpoints(self, graph, points)
4286 def key(self, c, x, y, width, height):
4287 raise RuntimeError("style doesn't yet provide a key")
4290 class arrow(symbol):
4292 def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
4293 self.linelength_str = linelength
4294 self.arrowsize_str = arrowsize
4295 self.arrowattrs = arrowattrs
4296 self.arrowdict = arrowdict
4297 self.epsilon = epsilon
4298 self.sizeindex = self.angleindex = None
4299 symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
4301 def iterate(self):
4302 raise RuntimeError("style is not iterateable")
4304 def othercolumnkey(self, key, index):
4305 if key == "size":
4306 self.sizeindex = index
4307 elif key == "angle":
4308 self.angleindex = index
4309 else:
4310 symbol.othercolumnkey(self, key, index)
4312 def _drawsymbol(self, graph, x, y, point=None):
4313 if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
4314 if point[self.sizeindex] > self.epsilon:
4315 dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
4316 x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
4317 y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
4318 x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
4319 y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
4320 graph.stroke(path.line(x1, y1, x2, y2),
4321 canvas.earrow(self.arrowsize*point[self.sizeindex],
4322 **self.arrowdict),
4323 *helper.ensuresequence(self.arrowattrs))
4325 def drawpoints(self, graph, points):
4326 self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
4327 self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
4328 self._arrowsize = unit.topt(self.arrowsize)
4329 self._linelength = unit.topt(self.linelength)
4330 if self.sizeindex is None:
4331 raise RuntimeError("column 'size' not set")
4332 if self.angleindex is None:
4333 raise RuntimeError("column 'angle' not set")
4334 symbol.drawpoints(self, graph, points)
4336 def key(self, c, x, y, width, height):
4337 raise RuntimeError("style doesn't yet provide a key")
4340 class _bariterator(changeattr):
4342 def attr(self, index):
4343 return index, self.counter
4346 class bar:
4348 def __init__(self, fromzero=1, stacked=0, skipmissing=1, xbar=0,
4349 barattrs=helper.nodefault, _usebariterator=helper.nodefault, _previousbar=None):
4350 self.fromzero = fromzero
4351 self.stacked = stacked
4352 self.skipmissing = skipmissing
4353 self.xbar = xbar
4354 if barattrs is helper.nodefault:
4355 self._barattrs = (canvas.stroked(color.gray.black), changecolor.Rainbow())
4356 else:
4357 self._barattrs = barattrs
4358 if _usebariterator is helper.nodefault:
4359 self.bariterator = _bariterator()
4360 else:
4361 self.bariterator = _usebariterator
4362 self.previousbar = _previousbar
4364 def iteratedict(self):
4365 result = {}
4366 result["barattrs"] = _iterateattrs(self._barattrs)
4367 return result
4369 def iterate(self):
4370 return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
4371 _usebariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
4373 def setcolumns(self, graph, columns):
4374 def checkpattern(key, index, pattern, iskey, isindex):
4375 if key is not None:
4376 match = pattern.match(key)
4377 if match:
4378 if isindex is not None: raise ValueError("multiple key specification")
4379 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4380 key = None
4381 iskey = match.groups()[0]
4382 isindex = index
4383 return key, iskey, isindex
4385 xkey = ykey = None
4386 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4387 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4388 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4389 xi = yi = None
4390 for key, index in columns.items():
4391 key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
4392 key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
4393 if key is not None:
4394 self.othercolumnkey(key, index)
4395 if None in (xkey, ykey): raise ValueError("incomplete axis specification")
4396 if self.xbar:
4397 self.nkey, self.ni = ykey, yi
4398 self.vkey, self.vi = xkey, xi
4399 else:
4400 self.nkey, self.ni = xkey, xi
4401 self.vkey, self.vi = ykey, yi
4402 self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
4404 def getranges(self, points):
4405 index, count = _getattr(self.bariterator)
4406 if count != 1 and self.stacked != 1:
4407 if self.stacked > 1:
4408 index = divmod(index, self.stacked)[0]
4410 vmin = vmax = None
4411 for point in points:
4412 if not self.skipmissing:
4413 if count != 1 and self.stacked != 1:
4414 self.naxis.setname(point[self.ni], index)
4415 else:
4416 self.naxis.setname(point[self.ni])
4417 try:
4418 v = point[self.vi] + 0.0
4419 if vmin is None or v < vmin: vmin = v
4420 if vmax is None or v > vmax: vmax = v
4421 except (TypeError, ValueError):
4422 pass
4423 else:
4424 if self.skipmissing:
4425 if count != 1 and self.stacked != 1:
4426 self.naxis.setname(point[self.ni], index)
4427 else:
4428 self.naxis.setname(point[self.ni])
4429 if self.fromzero:
4430 if vmin > 0: vmin = 0
4431 if vmax < 0: vmax = 0
4432 return {self.vkey: (vmin, vmax)}
4434 def drawpoints(self, graph, points):
4435 index, count = _getattr(self.bariterator)
4436 dostacked = (self.stacked != 0 and
4437 (self.stacked == 1 or divmod(index, self.stacked)[1]) and
4438 (self.stacked != 1 or index))
4439 if self.stacked > 1:
4440 index = divmod(index, self.stacked)[0]
4441 vmin, vmax = self.vaxis.getdatarange()
4442 self.barattrs = _getattrs(helper.ensuresequence(self._barattrs))
4443 if self.stacked:
4444 self.stackedvalue = {}
4445 for point in points:
4446 try:
4447 n = point[self.ni]
4448 v = point[self.vi]
4449 if self.stacked:
4450 self.stackedvalue[n] = v
4451 if count != 1 and self.stacked != 1:
4452 minid = (n, index, 0)
4453 maxid = (n, index, 1)
4454 else:
4455 minid = (n, 0)
4456 maxid = (n, 1)
4457 if self.xbar:
4458 x1pos, y1pos = graph._pos(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
4459 x2pos, y2pos = graph._pos(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4460 else:
4461 x1pos, y1pos = graph._pos(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
4462 x2pos, y2pos = graph._pos(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
4463 if dostacked:
4464 if self.xbar:
4465 x3pos, y3pos = graph._pos(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
4466 x4pos, y4pos = graph._pos(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
4467 else:
4468 x3pos, y3pos = graph._pos(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4469 x4pos, y4pos = graph._pos(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4470 else:
4471 if self.fromzero:
4472 if self.xbar:
4473 x3pos, y3pos = graph._pos(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4474 x4pos, y4pos = graph._pos(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
4475 else:
4476 x3pos, y3pos = graph._pos(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4477 x4pos, y4pos = graph._pos(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4478 else:
4479 x3pos, y3pos = self.naxis._vtickpoint(self.naxis, self.naxis.convert(maxid))
4480 x4pos, y4pos = self.naxis._vtickpoint(self.naxis, self.naxis.convert(minid))
4481 if self.barattrs is not None:
4482 graph.fill(path.path(path._moveto(x1pos, y1pos),
4483 graph._connect(x1pos, y1pos, x2pos, y2pos),
4484 graph._connect(x2pos, y2pos, x3pos, y3pos),
4485 graph._connect(x3pos, y3pos, x4pos, y4pos),
4486 graph._connect(x4pos, y4pos, x1pos, y1pos), # no closepath (might not be straight)
4487 path.closepath()), *self.barattrs)
4488 except (TypeError, ValueError): pass
4490 def key(self, c, x, y, width, height):
4491 c.fill(path._rect(x, y, width, height), *self.barattrs)
4494 #class surface:
4496 # def setcolumns(self, graph, columns):
4497 # self.columns = columns
4499 # def getranges(self, points):
4500 # return {"x": (0, 10), "y": (0, 10), "z": (0, 1)}
4502 # def drawpoints(self, graph, points):
4503 # pass
4507 ################################################################################
4508 # data
4509 ################################################################################
4512 class data:
4514 defaultstyle = symbol
4516 def __init__(self, file, title=helper.nodefault, context={}, **columns):
4517 self.title = title
4518 if helper.isstring(file):
4519 self.data = datamodule.datafile(file)
4520 else:
4521 self.data = file
4522 if title is helper.nodefault:
4523 self.title = "(unknown)"
4524 else:
4525 self.title = title
4526 self.columns = {}
4527 for key, column in columns.items():
4528 try:
4529 self.columns[key] = self.data.getcolumnno(column)
4530 except datamodule.ColumnError:
4531 self.columns[key] = len(self.data.titles)
4532 self.data.addcolumn(column, context=context)
4534 def setstyle(self, graph, style):
4535 self.style = style
4536 self.style.setcolumns(graph, self.columns)
4538 def getranges(self):
4539 return self.style.getranges(self.data.data)
4541 def setranges(self, ranges):
4542 pass
4544 def draw(self, graph):
4545 self.style.drawpoints(graph, self.data.data)
4548 class function:
4550 defaultstyle = line
4552 def __init__(self, expression, title=helper.nodefault, min=None, max=None, points=100, parser=mathtree.parser(), context={}):
4553 if title is helper.nodefault:
4554 self.title = expression
4555 else:
4556 self.title = title
4557 self.min = min
4558 self.max = max
4559 self.points = points
4560 self.context = context
4561 self.result, expression = expression.split("=")
4562 self.mathtree = parser.parse(expression)
4563 self.variable = None
4564 self.evalranges = 0
4566 def setstyle(self, graph, style):
4567 for variable in self.mathtree.VarList():
4568 if variable in graph.axes.keys():
4569 if self.variable is None:
4570 self.variable = variable
4571 else:
4572 raise ValueError("multiple variables found")
4573 if self.variable is None:
4574 raise ValueError("no variable found")
4575 self.xaxis = graph.axes[self.variable]
4576 self.style = style
4577 self.style.setcolumns(graph, {self.variable: 0, self.result: 1})
4579 def getranges(self):
4580 if self.evalranges:
4581 return self.style.getranges(self.data)
4582 if None not in (self.min, self.max):
4583 return {self.variable: (self.min, self.max)}
4585 def setranges(self, ranges):
4586 if ranges.has_key(self.variable):
4587 min, max = ranges[self.variable]
4588 if self.min is not None: min = self.min
4589 if self.max is not None: max = self.max
4590 vmin = self.xaxis.convert(min)
4591 vmax = self.xaxis.convert(max)
4592 self.data = []
4593 for i in range(self.points):
4594 self.context[self.variable] = x = self.xaxis.invert(vmin + (vmax-vmin)*i / (self.points-1.0))
4595 try:
4596 y = self.mathtree.Calc(**self.context)
4597 except (ArithmeticError, ValueError):
4598 y = None
4599 self.data.append((x, y))
4600 self.evalranges = 1
4602 def draw(self, graph):
4603 self.style.drawpoints(graph, self.data)
4606 class paramfunction:
4608 defaultstyle = line
4610 def __init__(self, varname, min, max, expression, title=helper.nodefault, points=100, parser=mathtree.parser(), context={}):
4611 if title is helper.nodefault:
4612 self.title = expression
4613 else:
4614 self.title = title
4615 self.varname = varname
4616 self.min = min
4617 self.max = max
4618 self.points = points
4619 self.expression = {}
4620 self.mathtrees = {}
4621 varlist, expressionlist = expression.split("=")
4622 parsestr = mathtree.ParseStr(expressionlist)
4623 for key in varlist.split(","):
4624 key = key.strip()
4625 if self.mathtrees.has_key(key):
4626 raise ValueError("multiple assignment in tuple")
4627 try:
4628 self.mathtrees[key] = parser.ParseMathTree(parsestr)
4629 break
4630 except mathtree.CommaFoundMathTreeParseError, e:
4631 self.mathtrees[key] = e.MathTree
4632 else:
4633 raise ValueError("unpack tuple of wrong size")
4634 if len(varlist.split(",")) != len(self.mathtrees.keys()):
4635 raise ValueError("unpack tuple of wrong size")
4636 self.data = []
4637 for i in range(self.points):
4638 context[self.varname] = self.min + (self.max-self.min)*i / (self.points-1.0)
4639 line = []
4640 for key, tree in self.mathtrees.items():
4641 line.append(tree.Calc(**context))
4642 self.data.append(line)
4644 def setstyle(self, graph, style):
4645 self.style = style
4646 columns = {}
4647 for key, index in zip(self.mathtrees.keys(), xrange(sys.maxint)):
4648 columns[key] = index
4649 self.style.setcolumns(graph, columns)
4651 def getranges(self):
4652 return self.style.getranges(self.data)
4654 def setranges(self, ranges):
4655 pass
4657 def draw(self, graph):
4658 self.style.drawpoints(graph, self.data)