axispos rework; splitaxis and baraxis finished for 0.4; completed the docstrings...
[PyX/mjg.git] / pyx / graph.py
blob33080f9d5634427069c7883b067a0c3ad6353d4f
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 # TODO: density == 1?
975 rate += self.tickrange.rate(axis.convert(float(ticks[-1]) * axis.divisor) -
976 axis.convert(float(ticks[0]) * axis.divisor), 1)
977 else:
978 rate += self.tickrange.rate(0, 1)
979 weight += self.tickrange.weight
980 return rate/weight
982 def ratelayout(self, axiscanvas, density):
983 """rate distances of the labels in an axis canvas
984 - the distances should be collected as box distances of
985 subsequent labels
986 - the axiscanvas provides a labels attribute for easy
987 access to the labels whose distances have to be taken
988 into account
989 - the density is used within the distancerate instance"""
990 if len(axiscanvas.labels) > 1:
991 try:
992 distances = [axiscanvas.labels[i]._boxdistance(axiscanvas.labels[i+1]) for i in range(len(axiscanvas.labels) - 1)]
993 except box.BoxCrossError:
994 return None
995 return self.distance.rate(distances, density)
996 else:
997 return 0
1000 ################################################################################
1001 # texter
1002 # texter automatically create labels for tick instances
1003 ################################################################################
1006 class _Itexter:
1008 def labels(self, ticks):
1009 """fill the label attribute of ticks
1010 - ticks is a list of instances of tick
1011 - for each element of ticks the value of the attribute label is set to
1012 a string appropriate to the attributes enum and denom of that tick
1013 instance
1014 - label attributes of the tick instances are just kept, whenever they
1015 are not equal to None
1016 - the method might extend the labelattrs attribute of the ticks"""
1019 class rationaltexter:
1020 "a texter creating rational labels (e.g. 'a/b' or even 'a \over b')"
1021 # XXX: we use divmod here to be more expicit
1023 __implements__ = _Itexter
1025 def __init__(self, prefix="", infix="", suffix="",
1026 enumprefix="", enuminfix="", enumsuffix="",
1027 denomprefix="", denominfix="", denomsuffix="",
1028 minus="-", minuspos=0, over=r"{{%s}\over{%s}}",
1029 equaldenom=0, skip1=1, skipenum0=1, skipenum1=1, skipdenom1=1,
1030 labelattrs=textmodule.mathmode):
1031 r"""initializes the instance
1032 - prefix, infix, and suffix (strings) are added at the begin,
1033 immediately after the minus, and at the end of the label,
1034 respectively
1035 - prefixenum, infixenum, and suffixenum (strings) are added
1036 to the labels enumerator correspondingly
1037 - prefixdenom, infixdenom, and suffixdenom (strings) are added
1038 to the labels denominator correspondingly
1039 - minus (string) is inserted for negative numbers
1040 - minuspos is an integer, which determines the position, where the
1041 minus sign has to be placed; the following values are allowed:
1042 1 - writes the minus in front of the enumerator
1043 0 - writes the minus in front of the hole fraction
1044 -1 - writes the minus in front of the denominator
1045 - over (string) is taken as a format string generating the
1046 fraction bar; it has to contain exactly two string insert
1047 operators "%s" -- the first for the enumerator and the second
1048 for the denominator; by far the most common examples are
1049 r"{{%s}\over{%s}}" and "{{%s}/{%s}}"
1050 - usually the enumerator and denominator are canceled; however,
1051 when equaldenom is set, the least common multiple of all
1052 denominators is used
1053 - skip1 (boolean) just prints the prefix, the minus (if present),
1054 the infix and the suffix, when the value is plus or minus one
1055 and at least one of prefix, infix and the suffix is present
1056 - skipenum0 (boolean) just prints a zero instead of
1057 the hole fraction, when the enumerator is zero;
1058 no prefixes, infixes, and suffixes are taken into account
1059 - skipenum1 (boolean) just prints the enumprefix, the minus (if present),
1060 the enuminfix and the enumsuffix, when the enum value is plus or minus one
1061 and at least one of enumprefix, enuminfix and the enumsuffix is present
1062 - skipdenom1 (boolean) just prints the enumerator instead of
1063 the hole fraction, when the denominator is one and none of the parameters
1064 denomprefix, denominfix and denomsuffix are set and minuspos is not -1 or the
1065 fraction is positive
1066 - labelattrs is a sequence of attributes for a texrunners text method;
1067 a single is allowed without being a sequence; None is considered as
1068 an empty sequence"""
1069 self.prefix = prefix
1070 self.infix = infix
1071 self.suffix = suffix
1072 self.enumprefix = enumprefix
1073 self.enuminfix = enuminfix
1074 self.enumsuffix = enumsuffix
1075 self.denomprefix = denomprefix
1076 self.denominfix = denominfix
1077 self.denomsuffix = denomsuffix
1078 self.minus = minus
1079 self.minuspos = minuspos
1080 self.over = over
1081 self.equaldenom = equaldenom
1082 self.skip1 = skip1
1083 self.skipenum0 = skipenum0
1084 self.skipenum1 = skipenum1
1085 self.skipdenom1 = skipdenom1
1086 self.labelattrs = helper.ensurelist(labelattrs)
1088 def gcd(self, *n):
1089 """returns the greates common divisor of all elements in n
1090 - the elements of n must be non-negative integers
1091 - return None if the number of elements is zero
1092 - the greates common divisor is not affected when some
1093 of the elements are zero, but it becomes zero when
1094 all elements are zero"""
1095 if len(n) == 2:
1096 i, j = n
1097 if i < j:
1098 i, j = j, i
1099 while j > 0:
1100 i, (dummy, j) = j, divmod(i, j)
1101 return i
1102 if len(n):
1103 res = n[0]
1104 for i in n[1:]:
1105 res = self.gcd(res, i)
1106 return res
1108 def lcm(self, *n):
1109 """returns the least common multiple of all elements in n
1110 - the elements of n must be non-negative integers
1111 - return None if the number of elements is zero
1112 - the least common multiple is zero when some of the
1113 elements are zero"""
1114 if len(n):
1115 res = n[0]
1116 for i in n[1:]:
1117 res = divmod(res * i, self.gcd(res, i))[0]
1118 return res
1120 def labels(self, ticks):
1121 labeledticks = []
1122 for tick in ticks:
1123 if tick.label is None and tick.labellevel is not None:
1124 labeledticks.append(tick)
1125 tick.temp_fracenum = tick.enum
1126 tick.temp_fracdenom = tick.denom
1127 tick.temp_fracminus = 1
1128 if tick.temp_fracenum < 0:
1129 tick.temp_fracminus *= -1
1130 tick.temp_fracenum *= -1
1131 if tick.temp_fracdenom < 0:
1132 tick.temp_fracminus *= -1
1133 tick.temp_fracdenom *= -1
1134 gcd = self.gcd(tick.temp_fracenum, tick.temp_fracdenom)
1135 (tick.temp_fracenum, dummy1), (tick.temp_fracdenom, dummy2) = divmod(tick.temp_fracenum, gcd), divmod(tick.temp_fracdenom, gcd)
1136 if self.equaldenom:
1137 equaldenom = self.lcm(*[tick.temp_fracdenom for tick in ticks if tick.label is None])
1138 if equaldenom is not None:
1139 for tick in labeledticks:
1140 factor, dummy = divmod(equaldenom, tick.temp_fracdenom)
1141 tick.temp_fracenum, tick.temp_fracdenom = factor * tick.temp_fracenum, factor * tick.temp_fracdenom
1142 for tick in labeledticks:
1143 fracminus = fracenumminus = fracdenomminus = ""
1144 if tick.temp_fracminus == -1:
1145 if self.minuspos == 0:
1146 fracminus = self.minus
1147 elif self.minuspos == 1:
1148 fracenumminus = self.minus
1149 elif self.minuspos == -1:
1150 fracdenomminus = self.minus
1151 else:
1152 raise RuntimeError("invalid minuspos")
1153 if self.skipenum0 and tick.temp_fracenum == 0:
1154 tick.label = "0"
1155 elif (self.skip1 and self.skipdenom1 and tick.temp_fracenum == 1 and tick.temp_fracdenom == 1 and
1156 (len(self.prefix) or len(self.infix) or len(self.suffix)) and
1157 not len(fracenumminus) and not len(self.enumprefix) and not len(self.enuminfix) and not len(self.enumsuffix) and
1158 not len(fracdenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix)):
1159 tick.label = "%s%s%s%s" % (self.prefix, fracminus, self.infix, self.suffix)
1160 else:
1161 if self.skipenum1 and tick.temp_fracenum == 1 and (len(self.enumprefix) or len(self.enuminfix) or len(self.enumsuffix)):
1162 tick.temp_fracenum = "%s%s%s%s" % (self.enumprefix, fracenumminus, self.enuminfix, self.enumsuffix)
1163 else:
1164 tick.temp_fracenum = "%s%s%s%i%s" % (self.enumprefix, fracenumminus, self.enuminfix, tick.temp_fracenum, self.enumsuffix)
1165 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):
1166 frac = tick.temp_fracenum
1167 else:
1168 tick.temp_fracdenom = "%s%s%s%i%s" % (self.denomprefix, fracdenomminus, self.denominfix, tick.temp_fracdenom, self.denomsuffix)
1169 frac = self.over % (tick.temp_fracenum, tick.temp_fracdenom)
1170 tick.label = "%s%s%s%s%s" % (self.prefix, fracminus, self.infix, frac, self.suffix)
1171 tick.labelattrs.extend(self.labelattrs)
1173 # del tick.temp_fracenum # we've inserted those temporary variables ... and do not care any longer about them
1174 # del tick.temp_fracdenom
1175 # del tick.temp_fracminus
1179 class decimaltexter:
1180 "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')"
1182 __implements__ = _Itexter
1184 def __init__(self, prefix="", infix="", suffix="", equalprecision=0,
1185 decimalsep=".", thousandsep="", thousandthpartsep="",
1186 minus="-", period=r"\overline{%s}", labelattrs=textmodule.mathmode):
1187 r"""initializes the instance
1188 - prefix, infix, and suffix (strings) are added at the begin,
1189 immediately after the minus, and at the end of the label,
1190 respectively
1191 - decimalsep, thousandsep, and thousandthpartsep (strings)
1192 are used as separators
1193 - minus (string) is inserted for negative numbers
1194 - period (string) is taken as a format string generating a period;
1195 it has to contain exactly one string insert operators "%s" for the
1196 period; usually it should be r"\overline{%s}"
1197 - labelattrs is a sequence of attributes for a texrunners text method;
1198 a single is allowed without being a sequence; None is considered as
1199 an empty sequence"""
1200 self.prefix = prefix
1201 self.infix = infix
1202 self.suffix = suffix
1203 self.equalprecision = equalprecision
1204 self.decimalsep = decimalsep
1205 self.thousandsep = thousandsep
1206 self.thousandthpartsep = thousandthpartsep
1207 self.minus = minus
1208 self.period = period
1209 self.labelattrs = helper.ensurelist(labelattrs)
1211 def labels(self, ticks):
1212 labeledticks = []
1213 maxdecprecision = 0
1214 for tick in ticks:
1215 if tick.label is None and tick.labellevel is not None:
1216 labeledticks.append(tick)
1217 m, n = tick.enum, tick.denom
1218 if m < 0: m *= -1
1219 if n < 0: n *= -1
1220 whole, reminder = divmod(m, n)
1221 whole = str(whole)
1222 if len(self.thousandsep):
1223 l = len(whole)
1224 tick.label = ""
1225 for i in range(l):
1226 tick.label += whole[i]
1227 if not ((l-i-1) % 3) and l > i+1:
1228 tick.label += self.thousandsep
1229 else:
1230 tick.label = whole
1231 if reminder:
1232 tick.label += self.decimalsep
1233 oldreminders = []
1234 tick.temp_decprecision = 0
1235 while (reminder):
1236 tick.temp_decprecision += 1
1237 if reminder in oldreminders:
1238 tick.temp_decprecision = None
1239 periodstart = len(tick.label) - (len(oldreminders) - oldreminders.index(reminder))
1240 tick.label = tick.label[:periodstart] + self.period % tick.label[periodstart:]
1241 break
1242 oldreminders += [reminder]
1243 reminder *= 10
1244 whole, reminder = divmod(reminder, n)
1245 if not ((tick.temp_decprecision - 1) % 3) and tick.temp_decprecision > 1:
1246 tick.label += self.thousandthpartsep
1247 tick.label += str(whole)
1248 if maxdecprecision < tick.temp_decprecision:
1249 maxdecprecision = tick.temp_decprecision
1250 if self.equalprecision:
1251 for tick in labeledticks:
1252 if tick.temp_decprecision is not None:
1253 if tick.temp_decprecision == 0 and maxdecprecision > 0:
1254 tick.label += self.decimalsep
1255 for i in range(tick.temp_decprecision, maxdecprecision):
1256 if not ((i - 1) % 3) and i > 1:
1257 tick.label += self.thousandthpartsep
1258 tick.label += "0"
1259 for tick in labeledticks:
1260 if tick.enum * tick.denom < 0:
1261 minus = self.minus
1262 else:
1263 minus = ""
1264 tick.label = "%s%s%s%s%s" % (self.prefix, minus, self.infix, tick.label, self.suffix)
1265 tick.labelattrs.extend(self.labelattrs)
1267 # del tick.temp_decprecision # we've inserted this temporary variable ... and do not care any longer about it
1270 class exponentialtexter:
1271 "a texter creating labels with exponentials (e.g. '2\cdot10^5')"
1273 __implements__ = _Itexter
1275 def __init__(self, plus="", minus="-",
1276 mantissaexp=r"{{%s}\cdot10^{%s}}",
1277 nomantissaexp=r"{10^{%s}}",
1278 minusnomantissaexp=r"{-10^{%s}}",
1279 mantissamin=frac((1, 1)), mantissamax=frac((10, 1)),
1280 skipmantissa1=0, skipallmantissa1=1,
1281 mantissatexter=decimaltexter()):
1282 r"""initializes the instance
1283 - plus or minus (string) is inserted for positive or negative exponents
1284 - mantissaexp (string) is taken as a format string generating the exponent;
1285 it has to contain exactly two string insert operators "%s" --
1286 the first for the mantissa and the second for the exponent;
1287 examples are r"{{%s}\cdot10^{%s}}" and r"{{%s}{\rm e}^{%s}}"
1288 - nomantissaexp (string) is taken as a format string generating the exponent
1289 when the mantissa is one and should be skipped; it has to contain
1290 exactly one string insert operators "%s" for the exponent;
1291 an examples is r"{10^{%s}}"
1292 - minusnomantissaexp (string) is taken as a format string generating the exponent
1293 when the mantissa is minus one and should be skipped; it has to contain
1294 exactly one string insert operators "%s" for the exponent; might be set to None
1295 to disallow skipping of any mantissa minus one
1296 an examples is r"{-10^{%s}}"
1297 - mantissamin and mantissamax are the minimum and maximum of the mantissa;
1298 they are frac instances greater than zero and mantissamin < mantissamax;
1299 the sign of the tick is ignored here
1300 - skipmantissa1 (boolean) turns on skipping of any mantissa equals one
1301 (and minus when minusnomantissaexp is set)
1302 - skipallmantissa1 (boolean) as above, but all mantissas must be 1
1303 - mantissatexter is the texter for the mantissa"""
1304 self.plus = plus
1305 self.minus = minus
1306 self.mantissaexp = mantissaexp
1307 self.nomantissaexp = nomantissaexp
1308 self.minusnomantissaexp = minusnomantissaexp
1309 self.mantissamin = mantissamin
1310 self.mantissamax = mantissamax
1311 self.mantissamindivmax = self.mantissamin / self.mantissamax
1312 self.mantissamaxdivmin = self.mantissamax / self.mantissamin
1313 self.skipmantissa1 = skipmantissa1
1314 self.skipallmantissa1 = skipallmantissa1
1315 self.mantissatexter = mantissatexter
1317 def labels(self, ticks):
1318 labeledticks = []
1319 for tick in ticks:
1320 if tick.label is None and tick.labellevel is not None:
1321 tick.temp_orgenum, tick.temp_orgdenom = tick.enum, tick.denom
1322 labeledticks.append(tick)
1323 tick.temp_exp = 0
1324 if tick.enum:
1325 while abs(tick) >= self.mantissamax:
1326 tick.temp_exp += 1
1327 x = tick * self.mantissamindivmax
1328 tick.enum, tick.denom = x.enum, x.denom
1329 while abs(tick) < self.mantissamin:
1330 tick.temp_exp -= 1
1331 x = tick * self.mantissamaxdivmin
1332 tick.enum, tick.denom = x.enum, x.denom
1333 if tick.temp_exp < 0:
1334 tick.temp_exp = "%s%i" % (self.minus, -tick.temp_exp)
1335 else:
1336 tick.temp_exp = "%s%i" % (self.plus, tick.temp_exp)
1337 self.mantissatexter.labels(labeledticks)
1338 if self.minusnomantissaexp is not None:
1339 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if abs(tick.enum) == abs(tick.denom)])
1340 else:
1341 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if tick.enum == tick.denom])
1342 for tick in labeledticks:
1343 if (self.skipallmantissa1 and allmantissa1 or
1344 (self.skipmantissa1 and (tick.enum == tick.denom or
1345 (tick.enum == -tick.denom and self.minusnomantissaexp is not None)))):
1346 if tick.enum == tick.denom:
1347 tick.label = self.nomantissaexp % tick.temp_exp
1348 else:
1349 tick.label = self.minusnomantissaexp % tick.temp_exp
1350 else:
1351 tick.label = self.mantissaexp % (tick.label, tick.temp_exp)
1352 tick.enum, tick.denom = tick.temp_orgenum, tick.temp_orgdenom
1354 # del tick.temp_orgenum # we've inserted those temporary variables ... and do not care any longer about them
1355 # del tick.temp_orgdenom
1356 # del tick.temp_exp
1359 class defaulttexter:
1360 "a texter creating decimal or exponential labels"
1362 __implements__ = _Itexter
1364 def __init__(self, smallestdecimal=frac((1, 1000)),
1365 biggestdecimal=frac((9999, 1)),
1366 equaldecision=1,
1367 decimaltexter=decimaltexter(),
1368 exponentialtexter=exponentialtexter()):
1369 r"""initializes the instance
1370 - smallestdecimal and biggestdecimal are the smallest and
1371 biggest decimal values, where the decimaltexter should be used;
1372 they are frac instances; the sign of the tick is ignored here;
1373 a tick at zero is considered for the decimaltexter as well
1374 - equaldecision (boolean) uses decimaltexter or exponentialtexter
1375 globaly (set) or for each tick separately (unset)
1376 - decimaltexter and exponentialtexter are texters to be used"""
1377 self.smallestdecimal = smallestdecimal
1378 self.biggestdecimal = biggestdecimal
1379 self.equaldecision = equaldecision
1380 self.decimaltexter = decimaltexter
1381 self.exponentialtexter = exponentialtexter
1383 def labels(self, ticks):
1384 decticks = []
1385 expticks = []
1386 for tick in ticks:
1387 if tick.label is None and tick.labellevel is not None:
1388 if not tick.enum or (abs(tick) >= self.smallestdecimal and abs(tick) <= self.biggestdecimal):
1389 decticks.append(tick)
1390 else:
1391 expticks.append(tick)
1392 if self.equaldecision:
1393 if len(expticks):
1394 self.exponentialtexter.labels(ticks)
1395 else:
1396 self.decimaltexter.labels(ticks)
1397 else:
1398 for tick in decticks:
1399 self.decimaltexter.labels([tick])
1400 for tick in expticks:
1401 self.exponentialtexter.labels([tick])
1404 ################################################################################
1405 # axis painter
1406 ################################################################################
1409 class axiscanvas(canvas._canvas):
1410 """axis canvas
1411 - an axis canvas is a regular canvas to be filled by
1412 a axispainters painter method
1413 - it contains a PyX length extent to be used for the
1414 alignment of additional axes; the axis extent should
1415 be filled by the axispainters painter method; you may
1416 grasp this as a size information comparable to a bounding
1418 - it contains a list of textboxes called labels which are
1419 used to rate the distances between the labels if needed
1420 by the axis later on; the painter method has not only to
1421 insert the labels into this canvas, but should also fill
1422 this list, when a rating of the distances should be
1423 performed by the axis"""
1425 # __implements__ = sole implementation
1427 def __init__(self, texrunner, *args, **kwargs):
1428 """initializes the instance
1429 - sets the texrunner
1430 - sets extent to zero
1431 - sets labels to an empty list"""
1432 canvas._canvas.__init__(self, *args, **kwargs)
1433 self.settexrunner(texrunner)
1434 self.extent = 0
1435 self.labels = []
1438 class rotatetext:
1439 """create rotations accordingly to tick directions
1440 - upsidedown rotations are suppressed by rotating them by another 180 degree"""
1442 # __implements__ = sole implementation
1444 def __init__(self, direction, epsilon=1e-10):
1445 """initializes the instance
1446 - direction is an angle to be used relative to the tick direction
1447 - epsilon is the value by which 90 degrees can be exceeded before
1448 an 180 degree rotation is added"""
1449 self.direction = direction
1450 self.epsilon = epsilon
1452 def trafo(self, dx, dy):
1453 """returns a rotation transformation accordingly to the tick direction
1454 - dx and dy are the direction of the tick"""
1455 direction = self.direction + math.atan2(dy, dx) * 180 / math.pi
1456 while (direction > 90 + self.epsilon):
1457 direction -= 180
1458 while (direction < -90 - self.epsilon):
1459 direction += 180
1460 return trafomodule.rotate(direction)
1463 paralleltext = rotatetext(-90)
1464 orthogonaltext = rotatetext(0)
1467 class _Iaxispainter:
1468 "class for painting axes"
1470 def paint(self, axispos, axis, axiscanvas):
1471 """paint the axis into the axiscanvas
1472 - axispos is an instance, which implements _Iaxispos to
1473 define the tick positions
1474 - the axis and should not be modified (we may
1475 add some temporary variables like axis.ticks[i].temp_xxx,
1476 which might be used just temporary) -- the idea is that
1477 all things can be used several times
1478 - also do not modify the instance (self) -- even this
1479 instance might be used several times; thus do not modify
1480 attributes like self.titleattrs etc. (just use a copy of it)
1481 - the method might access some additional attributes from
1482 the axis, e.g. the axis title -- the axis painter should
1483 document this behavior and rely on the availability of
1484 those attributes -> it becomes a question of the proper
1485 usage of the combination of axis & axispainter
1486 - the axiscanvas is a axiscanvas instance and should be
1487 filled with ticks, labels, title, etc.; note that the
1488 extent and labels instance variables should be filled as
1489 documented in the axiscanvas"""
1492 class axistitlepainter:
1493 """class for painting an axis title
1494 - the axis must have a title attribute when using this painter;
1495 this title might be None"""
1497 __implements__ = _Iaxispainter
1499 def __init__(self, titledist="0.3 cm",
1500 titleattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1501 titledirection=paralleltext,
1502 titlepos=0.5):
1503 """initialized the instance
1504 - titledist is a visual PyX length giving the distance
1505 of the title from the axis extent already there (a title might
1506 be added after labels or other things are plotted already)
1507 - labelattrs is a sequence of attributes for a texrunners text
1508 method; a single is allowed without being a sequence; None
1509 turns off the title
1510 - titledirection is an instance of rotatetext or None
1511 - titlepos is the position of the title in graph coordinates"""
1512 self.titledist_str = titledist
1513 self.titleattrs = titleattrs
1514 self.titledirection = titledirection
1515 self.titlepos = titlepos
1517 def paint(self, axispos, axis, axiscanvas):
1518 if axis.title is not None and self.titleattrs is not None:
1519 titledist = unit.length(self.titledist_str, default_type="v")
1520 x, y = axispos._vtickpoint(self.titlepos, axis=axis)
1521 dx, dy = axispos.vtickdirection(self.titlepos, axis=axis)
1522 titleattrs = helper.ensurelist(self.titleattrs)
1523 if self.titledirection is not None:
1524 titleattrs.append(self.titledirection.trafo(dx, dy))
1525 title = axiscanvas.texrunner._text(x, y, axis.title, *titleattrs)
1526 axiscanvas.extent += titledist
1527 title.linealign(axiscanvas.extent, dx, dy)
1528 axiscanvas.extent += title.extent(dx, dy)
1529 axiscanvas.insert(title)
1532 class axispainter(axistitlepainter):
1533 """class for painting the ticks and labels of an axis
1534 - the inherited titleaxispainter is used to paint the title of
1535 the axis
1536 - note that the type of the elements of ticks given as an argument
1537 of the paint method must be suitable for the tick position methods
1538 of the axis"""
1540 __implements__ = _Iaxispainter
1542 defaultticklengths = ["%0.5f cm" % (0.2*goldenmean**(-i)) for i in range(10)]
1544 def __init__(self, innerticklengths=defaultticklengths,
1545 outerticklengths=None,
1546 tickattrs=(),
1547 gridattrs=None,
1548 zerolineattrs=(),
1549 baselineattrs=canvas.linecap.square,
1550 labeldist="0.3 cm",
1551 labelattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1552 labeldirection=None,
1553 labelhequalize=0,
1554 labelvequalize=1,
1555 **kwargs):
1556 """initializes the instance
1557 - innerticklenths and outerticklengths are two sequences of
1558 visual PyX lengths for ticks, subticks, etc. plotted inside
1559 and outside of the graph; when a single value is given, it
1560 is used for all tick levels; None turns off ticks inside or
1561 outside of the graph
1562 - tickattrs are a sequence of stroke attributes for the ticks;
1563 a single entry is allowed without being a sequence; None turns
1564 off ticks
1565 - gridlineattrs are a sequence of sequences used as stroke
1566 attributes for ticks, subticks etc.; when a single sequence
1567 is given, it is used for ticks, subticks, etc.; a single
1568 entry is allowed without being a sequence; None turns off
1569 gridlines
1570 - zerolineattrs 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 zeroline
1573 - baselineattrs are a sequence of stroke attributes for a grid
1574 line at axis value zero; a single entry is allowed without
1575 being a sequence; None turns off the baseline
1576 - labeldist is a visual PyX length for the distance of the labels
1577 from the axis baseline
1578 - labelattrs is a sequence of attributes for a texrunners text
1579 method; a single entry is allowed without being a sequence;
1580 None turns off the labels
1581 - titledirection is an instance of rotatetext or None
1582 - labelhequalize and labelvequalize (booleans) perform an equal
1583 alignment for straight vertical and horizontal axes, respectively
1584 - futher keyword arguments are passed to axistitlepainter"""
1585 # TODO: access to axis.divisor -- document, remove, ... ???
1586 self.innerticklengths_str = innerticklengths
1587 self.outerticklengths_str = outerticklengths
1588 self.tickattrs = tickattrs
1589 self.gridattrs = gridattrs
1590 self.zerolineattrs = zerolineattrs
1591 self.baselineattrs = baselineattrs
1592 self.labeldist_str = labeldist
1593 self.labelattrs = labelattrs
1594 self.labeldirection = labeldirection
1595 self.labelhequalize = labelhequalize
1596 self.labelvequalize = labelvequalize
1597 axistitlepainter.__init__(self, **kwargs)
1599 def paint(self, axispos, axis, axiscanvas):
1600 labeldist = unit.length(self.labeldist_str, default_type="v")
1601 for tick in axis.ticks:
1602 tick.temp_v = axis.convert(float(tick) * axis.divisor)
1603 tick.temp_x, tick.temp_y = axispos._vtickpoint(tick.temp_v, axis=axis)
1604 tick.temp_dx, tick.temp_dy = axispos.vtickdirection(tick.temp_v, axis=axis)
1606 # create & align tick.temp_labelbox
1607 for tick in axis.ticks:
1608 if tick.labellevel is not None:
1609 labelattrs = helper.getsequenceno(self.labelattrs, tick.labellevel)
1610 if labelattrs is not None:
1611 labelattrs = helper.ensurelist(labelattrs)[:]
1612 if self.labeldirection is not None:
1613 labelattrs.append(self.labeldirection.trafo(tick.temp_dx, tick.temp_dy))
1614 if tick.labelattrs is not None:
1615 labelattrs.extend(helper.ensurelist(tick.labelattrs))
1616 tick.temp_labelbox = axiscanvas.texrunner._text(tick.temp_x, tick.temp_y, tick.label, *labelattrs)
1617 if len(axis.ticks) > 1:
1618 equaldirection = 1
1619 for tick in axis.ticks[1:]:
1620 if tick.temp_dx != axis.ticks[0].temp_dx or tick.temp_dy != axis.ticks[0].temp_dy:
1621 equaldirection = 0
1622 else:
1623 equaldirection = 0
1624 if equaldirection and ((not axis.ticks[0].temp_dx and self.labelvequalize) or
1625 (not axis.ticks[0].temp_dy and self.labelhequalize)):
1626 if self.labelattrs is not None:
1627 box.linealignequal([tick.temp_labelbox for tick in axis.ticks if tick.labellevel is not None],
1628 labeldist, axis.ticks[0].temp_dx, axis.ticks[0].temp_dy)
1629 else:
1630 for tick in axis.ticks:
1631 if tick.labellevel is not None and self.labelattrs is not None:
1632 tick.temp_labelbox.linealign(labeldist, tick.temp_dx, tick.temp_dy)
1634 def mkv(arg):
1635 if helper.issequence(arg):
1636 return [unit.length(a, default_type="v") for a in arg]
1637 if arg is not None:
1638 return unit.length(arg, default_type="v")
1639 innerticklengths = mkv(self.innerticklengths_str)
1640 outerticklengths = mkv(self.outerticklengths_str)
1642 for tick in axis.ticks:
1643 if tick.ticklevel is not None:
1644 innerticklength = helper.getitemno(innerticklengths, tick.ticklevel)
1645 outerticklength = helper.getitemno(outerticklengths, tick.ticklevel)
1646 if innerticklength is not None or outerticklength is not None:
1647 if innerticklength is None:
1648 innerticklength = 0
1649 if outerticklength is None:
1650 outerticklength = 0
1651 tickattrs = helper.getsequenceno(self.tickattrs, tick.ticklevel)
1652 if tickattrs is not None:
1653 _innerticklength = unit.topt(innerticklength)
1654 _outerticklength = unit.topt(outerticklength)
1655 x1 = tick.temp_x - tick.temp_dx * _innerticklength
1656 y1 = tick.temp_y - tick.temp_dy * _innerticklength
1657 x2 = tick.temp_x + tick.temp_dx * _outerticklength
1658 y2 = tick.temp_y + tick.temp_dy * _outerticklength
1659 axiscanvas.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(tickattrs))
1660 if tick != frac((0, 1)) or self.zerolineattrs is None:
1661 gridattrs = helper.getsequenceno(self.gridattrs, tick.ticklevel)
1662 if gridattrs is not None:
1663 axiscanvas.stroke(axispos.vgridline(tick.temp_v, axis=axis), *helper.ensuresequence(gridattrs))
1664 if outerticklength is not None and unit.topt(outerticklength) > unit.topt(axiscanvas.extent):
1665 axiscanvas.extent = outerticklength
1666 if outerticklength is not None and unit.topt(-innerticklength) > unit.topt(axiscanvas.extent):
1667 axiscanvas.extent = -innerticklength
1668 if tick.labellevel is not None and self.labelattrs is not None:
1669 axiscanvas.insert(tick.temp_labelbox)
1670 axiscanvas.labels.append(tick.temp_labelbox)
1671 extent = tick.temp_labelbox.extent(tick.temp_dx, tick.temp_dy) + labeldist
1672 if unit.topt(extent) > unit.topt(axiscanvas.extent):
1673 axiscanvas.extent = extent
1674 if self.baselineattrs is not None:
1675 axiscanvas.stroke(axispos.vbaseline(axis=axis), *helper.ensuresequence(self.baselineattrs))
1676 if self.zerolineattrs is not None:
1677 if len(axis.ticks) and axis.ticks[0] * axis.ticks[-1] < frac((0, 1)):
1678 axiscanvas.stroke(axispos.gridline(0, axis=axis), *helper.ensuresequence(self.zerolineattrs))
1680 # for tick in axis.ticks:
1681 # del tick.temp_v # we've inserted those temporary variables ... and do not care any longer about them
1682 # del tick.temp_x
1683 # del tick.temp_y
1684 # del tick.temp_dx
1685 # del tick.temp_dy
1686 # if tick.labellevel is not None and self.labelattrs is not None:
1687 # del tick.temp_labelbox
1689 axistitlepainter.paint(self, axispos, axis, axiscanvas)
1692 class linkaxispainter(axispainter):
1693 """class for painting a linked axis
1694 - the inherited axispainter is used to paint the axis
1695 - modifies some constructor defaults"""
1697 __implements__ = _Iaxispainter
1699 def __init__(self, gridattrs=None,
1700 zerolineattrs=None,
1701 labelattrs=None,
1702 titleattrs=None,
1703 **kwargs):
1704 """initializes the instance
1705 - the gridattrs default is set to None thus skipping the girdlines
1706 - the zerolineattrs default is set to None thus skipping the zeroline
1707 - the labelattrs default is set to None thus skipping the labels
1708 - the titleattrs default is set to None thus skipping the title
1709 - all keyword arguments are passed to axispainter"""
1710 axispainter.__init__(self, gridattrs=gridattrs,
1711 zerolineattrs=zerolineattrs,
1712 labelattrs=labelattrs,
1713 titleattrs=titleattrs,
1714 **kwargs)
1717 class splitaxispainter(axistitlepainter):
1718 """class for painting a splitaxis
1719 - the inherited titleaxispainter is used to paint the title of
1720 the axis
1721 - the splitaxispainter access the subaxes attribute of the axis"""
1723 __implements__ = _Iaxispainter
1725 def __init__(self, breaklinesdist="0.05 cm",
1726 breaklineslength="0.5 cm",
1727 breaklinesangle=-60,
1728 breaklinesattrs=(),
1729 **args):
1730 """initializes the instance
1731 - breaklinesdist is a visual length of the distance between
1732 the two lines of the axis break
1733 - breaklineslength is a visual length of the length of the
1734 two lines of the axis break
1735 - breaklinesangle is the angle of the lines of the axis break
1736 - breaklinesattrs are a sequence of stroke attributes for the
1737 axis break lines; a single entry is allowed without being a
1738 sequence; None turns off the break lines
1739 - futher keyword arguments are passed to axistitlepainter"""
1740 self.breaklinesdist_str = breaklinesdist
1741 self.breaklineslength_str = breaklineslength
1742 self.breaklinesangle = breaklinesangle
1743 self.breaklinesattrs = breaklinesattrs
1744 axistitlepainter.__init__(self, **args)
1746 def paint(self, axispos, axis, axiscanvas):
1747 for subaxis in axis.subaxes:
1748 subaxis.finish(axis, axiscanvas.texrunner)
1749 axiscanvas.insert(subaxis.axiscanvas)
1750 if unit.topt(axiscanvas.extent) < unit.topt(subaxis.axiscanvas.extent):
1751 axiscanvas.extent = subaxis.axiscanvas.extent
1752 if self.breaklinesattrs is not None:
1753 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1754 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1755 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1756 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1757 breaklinesextent = (0.5*self.breaklinesdist*math.fabs(self.cos) +
1758 0.5*self.breaklineslength*math.fabs(self.sin))
1759 if unit.topt(axiscanvas.extent) < unit.topt(breaklinesextent):
1760 axiscanvas.extent = breaklinesextent
1761 for subaxis1, subaxis2 in zip(axis.subaxes[:-1], axis.subaxes[1:]):
1762 # use a tangent of the baseline (this is independent of the tickdirection)
1763 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1764 breakline = path.normpath(axispos.vbaseline(v, None, axis=axis)).tangent(0, self.breaklineslength)
1765 widthline = path.normpath(axispos.vbaseline(v, None, axis=axis)).tangent(0, self.breaklinesdist).transformed(trafomodule.rotate(self.breaklinesangle+90, *breakline.begin()))
1766 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1767 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1768 breakline = breakline.transformed(trafomodule.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1769 breakline1 = breakline.transformed(trafomodule.translate(*towidth))
1770 breakline2 = breakline.transformed(trafomodule.translate(-towidth[0], -towidth[1]))
1771 axiscanvas.fill(path.path(path.moveto(*breakline1.begin()),
1772 path.lineto(*breakline1.end()),
1773 path.lineto(*breakline2.end()),
1774 path.lineto(*breakline2.begin()),
1775 path.closepath()), color.gray.white)
1776 axiscanvas.stroke(breakline1, *helper.ensuresequence(self.breaklinesattrs))
1777 axiscanvas.stroke(breakline2, *helper.ensuresequence(self.breaklinesattrs))
1778 axistitlepainter.paint(self, axispos, axis, axiscanvas)
1781 class linksplitaxispainter(splitaxispainter):
1782 """class for painting a linked splitaxis
1783 - the inherited splitaxispainter is used to paint the axis
1784 - modifies some constructor defaults"""
1786 __implements__ = _Iaxispainter
1788 def __init__(self, titleattrs=None, **kwargs):
1789 """initializes the instance
1790 - the titleattrs default is set to None thus skipping the title
1791 - all keyword arguments are passed to splitaxispainter"""
1792 splitaxispainter.__init__(self, titleattrs=titleattrs, **kwargs)
1795 class baraxispainter(axistitlepainter):
1796 """class for painting a baraxis
1797 - the inherited titleaxispainter is used to paint the title of
1798 the axis
1799 - the baraxispainter access the multisubaxis, subaxis names, texts, and
1800 relsizes attributes"""
1802 __implements__ = _Iaxispainter
1804 def __init__(self, innerticklength=None,
1805 outerticklength=None,
1806 tickattrs=(),
1807 baselineattrs=canvas.linecap.square,
1808 namedist="0.3 cm",
1809 nameattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1810 namedirection=None,
1811 namepos=0.5,
1812 namehequalize=0,
1813 namevequalize=1,
1814 **args):
1815 """initializes the instance
1816 - innerticklength and outerticklength are a visual length of
1817 the ticks to be plotted at the axis baseline to visually
1818 separate the bars; if neither innerticklength nor
1819 outerticklength are set, not ticks are plotted
1820 - breaklinesattrs are a sequence of stroke attributes for the
1821 axis tick; a single entry is allowed without being a
1822 sequence; None turns off the ticks
1823 - namedist is a visual PyX length for the distance of the bar
1824 names from the axis baseline
1825 - nameattrs is a sequence of attributes for a texrunners text
1826 method; a single entry is allowed without being a sequence;
1827 None turns off the names
1828 - namedirection is an instance of rotatetext or None
1829 - namehequalize and namevequalize (booleans) perform an equal
1830 alignment for straight vertical and horizontal axes, respectively
1831 - futher keyword arguments are passed to axistitlepainter"""
1832 self.innerticklength_str = innerticklength
1833 self.outerticklength_str = outerticklength
1834 self.tickattrs = tickattrs
1835 self.baselineattrs = baselineattrs
1836 self.namedist_str = namedist
1837 self.nameattrs = nameattrs
1838 self.namedirection = namedirection
1839 self.namepos = namepos
1840 self.namehequalize = namehequalize
1841 self.namevequalize = namevequalize
1842 axistitlepainter.__init__(self, **args)
1844 def paint(self, axispos, axis, axiscanvas):
1845 if axis.multisubaxis is not None:
1846 for subaxis in axis.subaxis:
1847 subaxis.finish(axis, axiscanvas.texrunner)
1848 axiscanvas.insert(subaxis.axiscanvas)
1849 if unit.topt(axiscanvas.extent) < unit.topt(subaxis.axiscanvas.extent):
1850 axiscanvas.extent = subaxis.axiscanvas.extent
1851 namepos = []
1852 for name in axis.names:
1853 v = axis.convert((name, self.namepos))
1854 x, y = axispos._vtickpoint(v, axis=axis)
1855 dx, dy = axispos.vtickdirection(v, axis=axis)
1856 namepos.append((v, x, y, dx, dy))
1857 nameboxes = []
1858 if self.nameattrs is not None:
1859 for (v, x, y, dx, dy), name in zip(namepos, axis.names):
1860 nameattrs = helper.ensurelist(self.nameattrs)[:]
1861 if self.namedirection is not None:
1862 nameattrs.append(self.namedirection.trafo(tick.temp_dx, tick.temp_dy))
1863 if axis.texts.has_key(name):
1864 nameboxes.append(axiscanvas.texrunner._text(x, y, str(axis.texts[name]), *nameattrs))
1865 elif axis.texts.has_key(str(name)):
1866 nameboxes.append(axiscanvas.texrunner._text(x, y, str(axis.texts[str(name)]), *nameattrs))
1867 else:
1868 nameboxes.append(axiscanvas.texrunner._text(x, y, str(name), *nameattrs))
1869 labeldist = axiscanvas.extent + unit.length(self.namedist_str, default_type="v")
1870 if len(namepos) > 1:
1871 equaldirection = 1
1872 for np in namepos[1:]:
1873 if np[3] != namepos[0][3] or np[4] != namepos[0][4]:
1874 equaldirection = 0
1875 else:
1876 equaldirection = 0
1877 if equaldirection and ((not namepos[0][3] and self.namevequalize) or
1878 (not namepos[0][4] and self.namehequalize)):
1879 box.linealignequal(nameboxes, labeldist, namepos[0][3], namepos[0][4])
1880 else:
1881 for namebox, np in zip(nameboxes, namepos):
1882 namebox.linealign(labeldist, np[3], np[4])
1883 if self.innerticklength_str is not None:
1884 innerticklength = unit.length(self.innerticklength_str, default_type="v")
1885 _innerticklength = unit.topt(innerticklength)
1886 if self.tickattrs is not None and unit.topt(axiscanvas.extent) < -_innerticklength:
1887 axiscanvas.extent = -innerticklength
1888 elif self.outerticklength_str is not None:
1889 innerticklength = _innerticklength = 0
1890 if self.outerticklength_str is not None:
1891 outerticklength = unit.length(self.outerticklength_str, default_type="v")
1892 _outerticklength = unit.topt(outerticklength)
1893 if self.tickattrs is not None and unit.topt(axiscanvas.extent) < _outerticklength:
1894 axiscanvas.extent = outerticklength
1895 elif self.innerticklength_str is not None:
1896 outerticklength = _outerticklength = 0
1897 for (v, x, y, dx, dy), namebox in zip(namepos, nameboxes):
1898 newextent = namebox.extent(dx, dy) + labeldist
1899 if unit.topt(axiscanvas.extent) < unit.topt(newextent):
1900 axiscanvas.extent = newextent
1901 if self.tickattrs is not None and (self.innerticklength_str is not None or self.outerticklength_str is not None):
1902 for pos in axis.relsizes:
1903 if pos == axis.relsizes[0]:
1904 pos -= axis.firstdist
1905 elif pos != axis.relsizes[-1]:
1906 pos -= 0.5 * axis.dist
1907 v = pos / axis.relsizes[-1]
1908 x, y = axispos._vtickpoint(v, axis=axis)
1909 dx, dy = axispos.vtickdirection(v, axis=axis)
1910 x1 = x - dx * _innerticklength
1911 y1 = y - dy * _innerticklength
1912 x2 = x + dx * _outerticklength
1913 y2 = y + dy * _outerticklength
1914 axiscanvas.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(self.tickattrs))
1915 if self.baselineattrs is not None:
1916 p = axispos.vbaseline(axis=axis)
1917 if p is not None:
1918 axiscanvas.stroke(axispos.vbaseline(axis=axis), *helper.ensuresequence(self.baselineattrs))
1919 for namebox in nameboxes:
1920 axiscanvas.insert(namebox)
1921 axistitlepainter.paint(self, axispos, axis, axiscanvas)
1924 class linkbaraxispainter(baraxispainter):
1925 """class for painting a linked baraxis
1926 - the inherited baraxispainter is used to paint the axis
1927 - modifies some constructor defaults"""
1929 __implements__ = _Iaxispainter
1931 def __init__(self, nameattrs=None, titleattrs=None, **kwargs):
1932 """initializes the instance
1933 - the titleattrs default is set to None thus skipping the title
1934 - the nameattrs default is set to None thus skipping the names
1935 - all keyword arguments are passed to axispainter"""
1936 baraxispainter.__init__(self, nameattrs=nameattrs, titleattrs=titleattrs, **kwargs)
1939 ################################################################################
1940 # axes
1941 ################################################################################
1944 class _Iaxis:
1945 """interface definition of a axis
1946 - data and tick range are handled separately
1947 - an axis should implement an convert and invert method like
1948 _Imap, but this is not part of this interface definition;
1949 one possibility is to mix-in a proper map class, but special
1950 purpose axes might do something else
1951 - instance variables:
1952 - axiscanvas: an axiscanvas instance, which is available after
1953 calling the finish method (the idea is, that an axis might
1954 fill different axiscanvas instances by calling the painter
1955 for different ticks and rate those ticks afterwards -- thus
1956 the axis finally can set this best canvas as its axis canvas)
1957 - relsize: relative size (width) of the axis (for use
1958 in splitaxis, baraxis etc.) -- this might not be available for
1959 all axes, which will just create an name error right now
1960 - configuration of the range handling is not subject
1961 of this interface definition"""
1962 # TODO: - add a mechanism to allow for a different range
1963 # handling of x and y ranges
1964 # - should we document instance variables this way?
1966 def convert(self, x):
1967 "convert a value into graph coordinates"
1969 def invert(self, v):
1970 "invert a graph coordinate to a axis value"
1972 def setdatarange(self, min, max):
1973 """set the axis data range
1974 - the type of min and max must fit to the axis
1975 - min<max; the axis might be reversed, but this is
1976 expressed internally only
1977 - the axis might not apply this change of the range
1978 (e.g. when the axis range is fixed by the user)
1979 - for invalid parameters (e.g. negativ values at an
1980 logarithmic axis), an exception should be raised"""
1981 # TODO: be more specific about exceptions
1983 def settickrange(self, min, max):
1984 """set the axis tick range
1985 - as before, but for the tick range (whatever this
1986 means for the axis)"""
1988 def getdatarange(self):
1989 """return data range as a tuple (min, max)
1990 - min<max; the axis might be reversed, but this is
1991 expressed internally only"""
1992 # TODO: be more specific about exceptions
1994 def gettickrange(self):
1995 """return tick range as a tuple (min, max)
1996 - as before, but for the tick range"""
1997 # TODO: be more specific about exceptions
1999 def finish(self, axispos, texrunner):
2000 """finishes the axis
2001 - axispos implements _Iaxispos (usually the graph itself
2002 can be used here)
2003 - the finish method creates an axiscanvas, which should be
2004 insertable into the graph to finally paint the axis
2005 - any modification of the axis range should be disabled after
2006 the finish method was called; a possible implementation
2007 would be to raise an error in these methods as soon as an
2008 axiscanvas is available"""
2009 # TODO: be more specific about exceptions
2011 def createlinkaxis(self, **kwargs):
2012 """create a link axis to the axis itself
2013 - typically, a link axis is a axis, which share almost
2014 all properties with the axis it is linked to
2015 - typically, the painter gets replaced by a painter
2016 which doesn't put any text to the axis"""
2019 class _Iaxispos:
2020 """interface definition of axis tick position methods
2021 - these methods are used for the postitioning of the ticks
2022 when painting an axis
2023 - you may try to avoid calling of these methods from each
2024 other to gain speed"""
2026 def baseline(self, x1=None, x2=None, axis=None):
2027 """return the baseline as a path
2028 - x1 is the start position; if not set, the baseline starts
2029 from the beginning of the axis, which might imply a
2030 value outside of the graph coordinate range [0; 1]
2031 - x2 is analogous to x1, but for the end position"""
2033 def vbaseline(self, v1=None, v2=None, axis=None):
2034 """return the baseline as a path
2035 - like baseline, but for graph coordinates"""
2037 def gridline(self, x, axis=None):
2038 "return the gridline as a path for a given position x"
2040 def vgridline(self, v, axis=None):
2041 """return the gridline as a path for a given position v
2042 in graph coordinates"""
2044 def _tickpoint(self, x, axis=None):
2045 """return the position at the baseline as a tuple (x, y) in
2046 postscript points for the position x"""
2048 def tickpoint(self, x, axis=None):
2049 """return the position at the baseline as a tuple (x, y) in
2050 in PyX length for the position x"""
2052 def _vtickpoint(self, v, axis=None):
2053 "like _tickpoint, but for graph coordinates"
2055 def vtickpoint(self, v, axis=None):
2056 "like tickpoint, but for graph coordinates"
2058 def tickdirection(self, x, axis=None):
2059 """return the direction of a tick as a tuple (dx, dy) for the
2060 position x"""
2062 def vtickdirection(self, v, axis=None):
2063 """like tickposition, but for graph coordinates"""
2066 class _axis:
2067 """base implementation a regular axis
2068 - typical usage is to mix-in a linmap or a logmap to
2069 complete the definition"""
2071 def __init__(self, min=None, max=None, reverse=0, divisor=1,
2072 datavmin=None, datavmax=None, tickvmin=0, tickvmax=1,
2073 title=None, painter=axispainter(), texter=defaulttexter(),
2074 density=1, maxworse=2):
2075 """initializes the instance
2076 - min and max fix the axis minimum and maximum, respectively;
2077 they are determined by the data to be plotted, when not fixed
2078 - reverse (boolean) reverses the minimum and the maximum of
2079 the axis
2080 - numerical divisor for the axis partitioning
2081 - datavmin and datavmax are the minimal and maximal graph
2082 coordinate when adjusting the axis to the data range
2083 (likely to be changed in the future)
2084 - as before, but for the tick range
2085 - title is a string containing the axis title
2086 - axispainter is the axis painter (should implement _Ipainter)
2087 - texter is the texter (should implement _Itexter)
2088 - density is a global parameter for the axis paritioning and
2089 axis rating; its default is 1, but the range 0.5 to 2.5 should
2090 be usefull to get less or more ticks by the automatic axis
2091 partitioning
2092 - maxworse is a number of trials with worse tick rating
2093 before giving up (usually it should not be needed to increase
2094 this value; increasing the number will slow down the automatic
2095 axis partitioning considerably)
2096 - note that some methods of this class want to access a
2097 part and a rating attribute of the instance; those
2098 attributes should be initialized by the constructors
2099 of derived classes"""
2100 # TODO: improve datavmin/datavmax/tickvmin/tickvmax
2101 if None not in (min, max) and min > max:
2102 min, max, reverse = max, min, not reverse
2103 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
2105 self.datamin = self.datamax = self.tickmin = self.tickmax = None
2106 if datavmin is None:
2107 if self.fixmin:
2108 self.datavmin = 0
2109 else:
2110 self.datavmin = 0.05
2111 else:
2112 self.datavmin = datavmin
2113 if datavmax is None:
2114 if self.fixmax:
2115 self.datavmax = 1
2116 else:
2117 self.datavmax = 0.95
2118 else:
2119 self.datavmax = datavmax
2120 self.tickvmin = tickvmin
2121 self.tickvmax = tickvmax
2123 self.divisor = divisor
2124 self.title = title
2125 self.painter = painter
2126 self.texter = texter
2127 self.density = density
2128 self.maxworse = maxworse
2129 self.axiscanvas = None
2130 self.canconvert = 0
2131 self.finished = 0
2132 self.__setinternalrange()
2134 def __setinternalrange(self, min=None, max=None):
2135 if not self.fixmin and min is not None and (self.min is None or min < self.min):
2136 self.min = min
2137 if not self.fixmax and max is not None and (self.max is None or max > self.max):
2138 self.max = max
2139 if None not in (self.min, self.max):
2140 min, max, vmin, vmax = self.min, self.max, 0, 1
2141 self.canconvert = 1
2142 self.setbasepoints(((min, vmin), (max, vmax)))
2143 if not self.fixmin:
2144 if self.datamin is not None and self.convert(self.datamin) < self.datavmin:
2145 min, vmin = self.datamin, self.datavmin
2146 self.setbasepoints(((min, vmin), (max, vmax)))
2147 if self.tickmin is not None and self.convert(self.tickmin) < self.tickvmin:
2148 min, vmin = self.tickmin, self.tickvmin
2149 self.setbasepoints(((min, vmin), (max, vmax)))
2150 if not self.fixmax:
2151 if self.datamax is not None and self.convert(self.datamax) > self.datavmax:
2152 max, vmax = self.datamax, self.datavmax
2153 self.setbasepoints(((min, vmin), (max, vmax)))
2154 if self.tickmax is not None and self.convert(self.tickmax) > self.tickvmax:
2155 max, vmax = self.tickmax, self.tickvmax
2156 self.setbasepoints(((min, vmin), (max, vmax)))
2157 if self.reverse:
2158 self.setbasepoints(((min, vmax), (max, vmin)))
2160 def __getinternalrange(self):
2161 return self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax
2163 def __forceinternalrange(self, range):
2164 self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax = range
2165 self.__setinternalrange()
2167 def setdatarange(self, min, max):
2168 if self.axiscanvas is not None:
2169 raise RuntimeError("axis was already finished")
2170 self.datamin, self.datamax = min, max
2171 self.__setinternalrange(min, max)
2173 def settickrange(self, min, max):
2174 if self.axiscanvas is not None:
2175 raise RuntimeError("axis was already finished")
2176 self.tickmin, self.tickmax = min, max
2177 self.__setinternalrange(min, max)
2179 def getdatarange(self):
2180 if self.canconvert:
2181 if self.reverse:
2182 return self.invert(1-self.datavmin), self.invert(1-self.datavmax)
2183 else:
2184 return self.invert(self.datavmin), self.invert(self.datavmax)
2186 def gettickrange(self):
2187 if self.canconvert:
2188 if self.reverse:
2189 return self.invert(1-self.tickvmin), self.invert(1-self.tickvmax)
2190 else:
2191 return self.invert(self.tickvmin), self.invert(self.tickvmax)
2193 def checkfraclist(self, fracs):
2194 "orders a list of fracs, equal entries are not allowed"
2195 if not len(fracs): return []
2196 sorted = list(fracs)
2197 sorted.sort()
2198 last = sorted[0]
2199 for item in sorted[1:]:
2200 if last == item:
2201 raise ValueError("duplicate entry found")
2202 last = item
2203 return sorted
2205 def finish(self, axispos, texrunner):
2206 if self.finished:
2207 return
2208 self.finished = 1
2210 min, max = self.gettickrange()
2211 parter = parterpos = None
2212 if self.part is not None:
2213 self.part = helper.ensurelist(self.part)
2214 for p, i in zip(self.part, xrange(sys.maxint)):
2215 if hasattr(p, "defaultpart"):
2216 if parter is not None:
2217 raise RuntimeError("only one partitioner allowed")
2218 parter = p
2219 parterpos = i
2220 if parter is None:
2221 self.ticks = self.checkfraclist(self.part)
2222 else:
2223 self.part[:parterpos] = self.checkfraclist(self.part[:parterpos])
2224 self.part[parterpos+1:] = self.checkfraclist(self.part[parterpos+1:])
2225 self.ticks = _mergeticklists(
2226 _mergeticklists(self.part[:parterpos],
2227 parter.defaultpart(min/self.divisor,
2228 max/self.divisor,
2229 not self.fixmin,
2230 not self.fixmax)),
2231 self.part[parterpos+1:])
2232 else:
2233 self.ticks = []
2234 # lesspart and morepart can be called after defaultpart;
2235 # this works although some axes may share their autoparting,
2236 # because the axes are processed sequentially
2237 first = 1
2238 worse = 0
2239 while worse < self.maxworse:
2240 if parter is not None:
2241 newticks = parter.lesspart()
2242 if parterpos is not None:
2243 newticks = _mergeticklists(_mergeticklists(self.part[:parterpos], newticks), self.part[parterpos+1:])
2244 else:
2245 newticks = None
2246 if newticks is not None:
2247 if first:
2248 bestrate = self.rater.ratepart(self, self.ticks, self.density)
2249 variants = [[bestrate, self.ticks]]
2250 first = 0
2251 newrate = self.rater.ratepart(self, newticks, self.density)
2252 variants.append([newrate, newticks])
2253 if newrate < bestrate:
2254 bestrate = newrate
2255 worse = 0
2256 else:
2257 worse += 1
2258 else:
2259 worse += 1
2260 worse = 0
2261 while worse < self.maxworse:
2262 if parter is not None:
2263 newticks = parter.morepart()
2264 if parterpos is not None:
2265 newticks = _mergeticklists(_mergeticklists(self.part[:parterpos], newticks), self.part[parterpos+1:])
2266 else:
2267 newticks = None
2268 if newticks is not None:
2269 if first:
2270 bestrate = self.rater.ratepart(self, self.ticks, self.density)
2271 variants = [[bestrate, self.ticks]]
2272 first = 0
2273 newrate = self.rater.ratepart(self, newticks, self.density)
2274 variants.append([newrate, newticks])
2275 if newrate < bestrate:
2276 bestrate = newrate
2277 worse = 0
2278 else:
2279 worse += 1
2280 else:
2281 worse += 1
2283 if not first:
2284 variants.sort()
2285 if self.painter is not None:
2286 i = 0
2287 bestrate = None
2288 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
2289 saverange = self.__getinternalrange()
2290 self.ticks = variants[i][1]
2291 if len(self.ticks):
2292 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2293 ac = axiscanvas(texrunner)
2294 self.texter.labels(self.ticks)
2295 self.painter.paint(axispos, self, ac)
2296 ratelayout = self.rater.ratelayout(ac, self.density)
2297 if ratelayout is not None:
2298 variants[i][0] += ratelayout
2299 variants[i].append(ac)
2300 else:
2301 variants[i][0] = None
2302 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
2303 bestrate = variants[i][0]
2304 self.__forceinternalrange(saverange)
2305 i += 1
2306 if bestrate is None:
2307 raise RuntimeError("no valid axis partitioning found")
2308 variants = [variant for variant in variants[:i] if variant[0] is not None]
2309 variants.sort()
2310 self.ticks = variants[0][1]
2311 if len(self.ticks):
2312 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2313 self.axiscanvas = variants[0][2]
2314 else:
2315 if len(self.ticks):
2316 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2317 self.axiscanvas = axiscanvas(texrunner)
2318 else:
2319 if len(self.ticks):
2320 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2321 self.axiscanvas = axiscanvas(texrunner)
2322 self.texter.labels(self.ticks)
2323 self.painter.paint(axispos, self, self.axiscanvas)
2325 def createlinkaxis(self, **args):
2326 return linkaxis(self, **args)
2329 class linaxis(_axis, _linmap):
2330 """implementation of a linear axis"""
2332 __implements__ = _Iaxis
2334 def __init__(self, part=autolinpart(), rater=axisrater(), **args):
2335 """initializes the instance
2336 - the part attribute contains a list of one partitioner
2337 (a partitioner implements _Ipart) or/and some (manually
2338 set) ticks (implementing _Itick); a single entry might
2339 be passed without wrapping it into a list; the partitioner
2340 and the tick instances must fit to the type of the axis
2341 (e.g. they should be valid parameters to the axis convert
2342 method); the ticks and the partitioner results are mixed
2343 by _mergeticklists
2344 - the rater implements _Irater and is used to rate different
2345 tick lists created by the partitioner (after merging with
2346 manully set ticks)
2347 - futher keyword arguments are passed to _axis"""
2348 _axis.__init__(self, **args)
2349 if self.fixmin and self.fixmax:
2350 self.relsize = self.max - self.min
2351 self.part = part
2352 self.rater = rater
2355 class logaxis(_axis, _logmap):
2356 """implementation of a logarithmic axis"""
2358 __implements__ = _Iaxis
2360 def __init__(self, part=autologpart(), rater=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
2361 """initializes the instance
2362 - the part attribute contains a list of one partitioner
2363 (a partitioner implements _Ipart) or/and some (manually
2364 set) ticks (implementing _Itick); a single entry might
2365 be passed without wrapping it into a list; the partitioner
2366 and the tick instances must fit to the type of the axis
2367 (e.g. they should be valid parameters to the axis convert
2368 method); the ticks and the partitioner results are mixed
2369 by _mergeticklists
2370 - the rater implements _Irater and is used to rate different
2371 tick lists created by the partitioner (after merging with
2372 manully set ticks)
2373 - futher keyword arguments are passed to _axis"""
2374 _axis.__init__(self, **args)
2375 if self.fixmin and self.fixmax:
2376 self.relsize = math.log(self.max) - math.log(self.min)
2377 self.part = part
2378 self.rater = rater
2381 class linkaxis:
2382 """a axis linked to an already existing regular axis
2383 - almost all properties of the axis are "copied" from the
2384 axis this axis is linked to
2385 - usually, linked axis are used to create an axis to an
2386 existing axis with different painting properties; linked
2387 axis can be used to plot an axis twice at the opposite
2388 sides of a graphxy or even to share an axis between
2389 different graphs!"""
2391 __implements__ = _Iaxis
2393 def __init__(self, linkedaxis, painter=linkaxispainter()):
2394 """initializes the instance
2395 - it gets a axis this linkaxis is linked to
2396 - it gets a painter to be used for this linked axis"""
2397 self.linkedaxis = linkedaxis
2398 self.painter = painter
2399 self.finished = 0
2401 def __getattr__(self, attr):
2402 """access to unkown attributes are handed over to the
2403 axis this linkaxis is linked to"""
2404 return getattr(self.linkedaxis, attr)
2406 def finish(self, axispos, texrunner):
2407 """finishes the axis
2408 - instead of performing the hole finish process
2409 (paritioning, rating, etc.) just a painter call
2410 is performed"""
2411 if self.finished:
2412 return
2413 self.finished = 1
2414 self.linkedaxis.finish(axispos, texrunner)
2415 self.axiscanvas = axiscanvas(texrunner)
2416 self.painter.paint(axispos, self, self.axiscanvas)
2419 class _subaxispos:
2420 """implementation of the _Iaxispos interface for subaxis
2421 - provides the _Iaxispos interface for axis which contain
2422 several subaxes
2423 - typical usage by mix-in this class into an "main" axis and
2424 calling the painter(s) of the subaxes from the specialized
2425 painter of the "main" axis; the subaxes need to have the
2426 attributes "baseaxis" (reference to the "main" axis) and
2427 "baseaxispos" (reference to the instance implementing the
2428 _Iaxispos interface of the "main" axis)"""
2430 __implements__ = _Iaxispos
2432 def baseline(self, x1=None, x2=None, axis=None):
2433 if x1 is not None:
2434 v1 = axis.convert(x1)
2435 else:
2436 v1 = 0
2437 if x2 is not None:
2438 v2 = axis.convert(x2)
2439 else:
2440 v2 = 1
2441 return axis.baseaxispos.vbaseline(v1, v2, axis=axis.baseaxis)
2443 def vbaseline(self, v1=None, v2=None, axis=None):
2444 if v1 is None:
2445 left = axis.vmin
2446 else:
2447 left = axis.vmin+v1*(axis.vmax-axis.vmin)
2448 if v2 is None:
2449 right = axis.vmax
2450 else:
2451 right = axis.vmin+v2*(axis.vmax-axis.vmin)
2452 return axis.baseaxispos.vbaseline(left, right, axis=axis.baseaxis)
2454 def gridline(self, x, axis=None):
2455 return axis.baseaxispos.vgridline(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2457 def vgridline(self, v, axis=None):
2458 return axis.baseaxispos.vgridline(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2460 def _tickpoint(self, x, axis=None):
2461 return axis.baseaxispos._vtickpoint(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2463 def tickpoint(self, x, axis=None):
2464 return axis.baseaxispos.vtickpoint(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2466 def _vtickpoint(self, v, axis=None):
2467 return axis.baseaxispos._vtickpoint(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2469 def vtickpoint(self, v, axis=None):
2470 return axis.baseaxispos.vtickpoint(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2472 def tickdirection(self, x, axis=None):
2473 return axis.baseaxispos.vtickdirection(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2475 def vtickdirection(self, v, axis=None):
2476 return axis.baseaxispos.vtickdirection(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2479 class splitaxis(_subaxispos):
2480 """implementation of a split axis
2481 - a split axis contains several (sub-)axes with
2482 non-overlapping data ranges -- between these subaxes
2483 the axis is "splitted"
2484 - (just to get sure: a splitaxis can contain other
2485 splitaxes as its subaxes)
2486 - a splitaxis implements the _Iaxispos for its subaxes
2487 by inheritance from _subaxispos"""
2489 __implements__ = _Iaxis, _Iaxispos
2491 def __init__(self, subaxes, splitlist=0.5, splitdist=0.1, relsizesplitdist=1,
2492 title=None, painter=splitaxispainter()):
2493 """initializes the instance
2494 - subaxes is a list of subaxes
2495 - splitlist is a list of graph coordinates, where the splitting
2496 of the main axis should be performed; a single entry (splitting
2497 two axes) doesn't need to be wrapped into a list; if the list
2498 isn't long enough for the subaxes, missing entries are considered
2499 to be None;
2500 - splitdist is the size of the splitting in graph coordinates, when
2501 the associated splitlist entry is not None
2502 - relsizesplitdist: a None entry in splitlist means, that the
2503 position of the splitting should be calculated out of the
2504 relsize values of conrtibuting subaxes (the size of the
2505 splitting is relsizesplitdist in values of the relsize values
2506 of the axes)
2507 - title is the title of the axis as a string
2508 - painter is the painter of the axis; it should be specialized to
2509 the splitaxis
2510 - the relsize of the splitaxis is the sum of the relsizes of the
2511 subaxes including the relsizesplitdist"""
2512 self.subaxes = subaxes
2513 self.painter = painter
2514 self.title = title
2515 self.splitlist = helper.ensurelist(splitlist)
2516 for subaxis in self.subaxes:
2517 subaxis.vmin = None
2518 subaxis.vmax = None
2519 self.subaxes[0].vmin = 0
2520 self.subaxes[0].vminover = None
2521 self.subaxes[-1].vmax = 1
2522 self.subaxes[-1].vmaxover = None
2523 for i in xrange(len(self.splitlist)):
2524 if self.splitlist[i] is not None:
2525 self.subaxes[i].vmax = self.splitlist[i] - 0.5*splitdist
2526 self.subaxes[i].vmaxover = self.splitlist[i]
2527 self.subaxes[i+1].vmin = self.splitlist[i] + 0.5*splitdist
2528 self.subaxes[i+1].vminover = self.splitlist[i]
2529 i = 0
2530 while i < len(self.subaxes):
2531 if self.subaxes[i].vmax is None:
2532 j = relsize = relsize2 = 0
2533 while self.subaxes[i + j].vmax is None:
2534 relsize += self.subaxes[i + j].relsize + relsizesplitdist
2535 j += 1
2536 relsize += self.subaxes[i + j].relsize
2537 vleft = self.subaxes[i].vmin
2538 vright = self.subaxes[i + j].vmax
2539 for k in range(i, i + j):
2540 relsize2 += self.subaxes[k].relsize
2541 self.subaxes[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
2542 relsize2 += 0.5 * relsizesplitdist
2543 self.subaxes[k].vmaxover = self.subaxes[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
2544 relsize2 += 0.5 * relsizesplitdist
2545 self.subaxes[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
2546 if i == 0 and i + j + 1 == len(self.subaxes):
2547 self.relsize = relsize
2548 i += j + 1
2549 else:
2550 i += 1
2552 self.fixmin = self.subaxes[0].fixmin
2553 if self.fixmin:
2554 self.min = self.subaxes[0].min
2555 self.fixmax = self.subaxes[-1].fixmax
2556 if self.fixmax:
2557 self.max = self.subaxes[-1].max
2558 self.finished = 0
2560 def getdatarange(self):
2561 min = self.subaxes[0].getdatarange()
2562 max = self.subaxes[-1].getdatarange()
2563 try:
2564 return min[0], max[1]
2565 except TypeError:
2566 return None
2568 def setdatarange(self, min, max):
2569 self.subaxes[0].setdatarange(min, None)
2570 self.subaxes[-1].setdatarange(None, max)
2572 def gettickrange(self):
2573 min = self.subaxes[0].gettickrange()
2574 max = self.subaxes[-1].gettickrange()
2575 try:
2576 return min[0], max[1]
2577 except TypeError:
2578 return None
2580 def settickrange(self, min, max):
2581 self.subaxes[0].settickrange(min, None)
2582 self.subaxes[-1].settickrange(None, max)
2584 def convert(self, value):
2585 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2586 if value < self.subaxes[0].max:
2587 return self.subaxes[0].vmin + self.subaxes[0].convert(value)*(self.subaxes[0].vmax-self.subaxes[0].vmin)
2588 for axis in self.subaxes[1:-1]:
2589 if value > axis.min and value < axis.max:
2590 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
2591 if value > self.subaxes[-1].min:
2592 return self.subaxes[-1].vmin + self.subaxes[-1].convert(value)*(self.subaxes[-1].vmax-self.subaxes[-1].vmin)
2593 raise ValueError("value couldn't be assigned to a split region")
2595 def vbaseline(self, v1=None, v2=None, axis=None):
2596 if v1 is None:
2597 if axis.baseaxis.painter.breaklinesattrs is None: # XXX undocumented access to painter!?
2598 left = axis.vmin
2599 else:
2600 if axis.vminover is None:
2601 left = None
2602 else:
2603 left = axis.vminover
2604 else:
2605 left = axis.vmin+v1*(axis.vmax-axis.vmin)
2606 if v2 is None:
2607 if axis.baseaxis.painter.breaklinesattrs is None:
2608 right = axis.vmax
2609 else:
2610 if axis.vmaxover is None:
2611 right = None
2612 else:
2613 right = axis.vmaxover
2614 else:
2615 right = axis.vmin+v2*(axis.vmax-axis.vmin)
2616 return axis.baseaxispos.vbaseline(left, right, axis=axis.baseaxis)
2618 def finish(self, axispos, texrunner):
2619 if self.finished:
2620 return
2621 self.finished = 1
2622 for subaxis in self.subaxes:
2623 subaxis.baseaxispos = axispos
2624 subaxis.baseaxis = self
2625 self.axiscanvas = axiscanvas(texrunner)
2626 self.painter.paint(axispos, self, self.axiscanvas)
2628 def createlinkaxis(self, **args):
2629 return linksplitaxis(self, **args)
2632 class linksplitaxis(linkaxis):
2633 """a splitaxis linked to an already existing splitaxis
2634 - inherits the access to a linked axis -- as before,
2635 basically only the painter is replaced
2636 - it must take care of the creation of linked axes of
2637 the subaxes"""
2639 __implements__ = _Iaxis
2641 def __init__(self, linkedaxis, painter=linksplitaxispainter(), subaxispainter=None):
2642 """initializes the instance
2643 - it gets a axis this linkaxis is linked to
2644 - it gets a painter to be used for this linked axis
2645 - it gets a list of painters to be used for the linkaxes
2646 of the subaxes; if None, the createlinkaxis of the subaxes
2647 are called without a painter parameter; if it is not a
2648 sequence, the subaxispainter is passed as the painter
2649 parameter to all createlinkaxis of the subaxes"""
2650 if subaxispainter is not None:
2651 if _issequence(subaxispainter):
2652 if len(linkedaxis.subaxes) != len(subaxispainter):
2653 raise RuntimeError("subaxes and subaxispainter lengths do not fit")
2654 self.subaxes = [a.createlinkaxis(painter=p) for a, p in zip(linkedaxis.subaxes, subaxispainter)]
2655 else:
2656 self.subaxes = [a.createlinkaxis(painter=subaxispainter) for a in linkedaxis.subaxes]
2657 else:
2658 self.subaxes = [a.createlinkaxis() for a in linkedaxis.subaxes]
2659 linkaxis.__init__(self, linkedaxis, painter=painter)
2661 def finish(self, axispos, texrunner):
2662 for subaxis in self.subaxes:
2663 subaxis.baseaxispos = axispos
2664 subaxis.baseaxis = self
2665 linkaxis.finish(self, axispos, texrunner)
2668 class baraxis(_subaxispos):
2669 """implementation of a axis for bar graphs
2670 - a bar axes is different from a splitaxis by the way it
2671 selects its subaxes: the convert method gets a list,
2672 where the first entry is a name selecting a subaxis out
2673 of a list; instead of the term "bar" or "subaxis" the term
2674 "item" will be used here
2675 - the baraxis stores a list of names be identify the items;
2676 the names might be of any time (strings, integers, etc.);
2677 the names can be printed as the titles for the items, but
2678 alternatively the names might be transformed by the texts
2679 dictionary, which maps a name to a text to be used to label
2680 the items in the painter
2681 - usually, there is only one subaxis, which is used as
2682 the subaxis for all items
2683 - alternatively it is also possible to use another baraxis
2684 as a multisubaxis; it is copied via the createsubaxis
2685 method whenever another subaxis is needed (by that a
2686 nested bar axis with a different number of subbars at
2687 each item can be created)
2688 - any axis can be a subaxis of a baraxis; if no subaxis
2689 is specified at all, the baraxis simulates a linear
2690 subaxis with a fixed range of 0 to 1
2691 - a splitaxis implements the _Iaxispos for its subaxes
2692 by inheritance from _subaxispos when the multisubaxis
2693 feature is turned on"""
2695 def __init__(self, subaxis=None, multisubaxis=None, title=None,
2696 dist=0.5, firstdist=None, lastdist=None, names=None,
2697 texts={}, painter=baraxispainter()):
2698 """initialize the instance
2699 - subaxis contains a axis to be used as the subaxis
2700 for all bars
2701 - multisubaxis might contain another baraxis instance
2702 to be used to construct a new subaxis for each item;
2703 (by that a nested bar axis with a different number
2704 of subbars at each item can be created)
2705 - only one of subaxis or multisubaxis can be set; if neither
2706 of them is set, the baraxis behaves like having a linaxis
2707 as its subaxis with a fixed range 0 to 1
2708 - the title attribute contains the axis title as a string
2709 - the dist is a relsize to be used as the distance between
2710 the items
2711 - the firstdist and lastdist are the distance before the
2712 first and after the last item, respectively; when set
2713 to None (the default), 0.5*dist is used
2714 - names is a predefined list of names to identify the
2715 items; if set, the name list is fixed
2716 - texts is a dictionary transforming a name to a text in
2717 the painter; if a name isn't found in the dictionary
2718 it gets used itself
2719 - the relsize of the baraxis is the sum of the
2720 relsizes including all distances between the items"""
2721 self.dist = dist
2722 if firstdist is not None:
2723 self.firstdist = firstdist
2724 else:
2725 self.firstdist = 0.5 * dist
2726 if lastdist is not None:
2727 self.lastdist = lastdist
2728 else:
2729 self.lastdist = 0.5 * dist
2730 self.relsizes = None
2731 self.fixnames = 0
2732 self.names = []
2733 for name in helper.ensuresequence(names):
2734 self.setname(name)
2735 self.fixnames = names is not None
2736 self.multisubaxis = multisubaxis
2737 if self.multisubaxis is not None:
2738 if subaxis is not None:
2739 raise RuntimeError("either use subaxis or multisubaxis")
2740 self.subaxis = [self.createsubaxis() for name in self.names]
2741 else:
2742 self.subaxis = subaxis
2743 self.title = title
2744 self.fixnames = 0
2745 self.texts = texts
2746 self.painter = painter
2748 def createsubaxis(self):
2749 return baraxis(subaxis=self.multisubaxis.subaxis,
2750 multisubaxis=self.multisubaxis.multisubaxis,
2751 title=self.multisubaxis.title,
2752 dist=self.multisubaxis.dist,
2753 firstdist=self.multisubaxis.firstdist,
2754 lastdist=self.multisubaxis.lastdist,
2755 names=self.multisubaxis.names,
2756 texts=self.multisubaxis.texts,
2757 painter=self.multisubaxis.painter)
2759 def getdatarange(self):
2760 # TODO: we do not yet have a proper range handling for a baraxis
2761 return None
2763 def gettickrange(self):
2764 # TODO: we do not yet have a proper range handling for a baraxis
2765 raise RuntimeError("range handling for a baraxis is not implemented")
2767 def setdatarange(self):
2768 # TODO: we do not yet have a proper range handling for a baraxis
2769 raise RuntimeError("range handling for a baraxis is not implemented")
2771 def settickrange(self):
2772 # TODO: we do not yet have a proper range handling for a baraxis
2773 raise RuntimeError("range handling for a baraxis is not implemented")
2775 def setname(self, name, *subnames):
2776 """add a name to identify an item at the baraxis
2777 - by using subnames, nested name definitions are
2778 possible
2779 - a style (or the user itself) might use this to
2780 insert new items into a baraxis
2781 - setting self.relsizes to None forces later recalculation"""
2782 if not self.fixnames:
2783 if name not in self.names:
2784 self.relsizes = None
2785 self.names.append(name)
2786 if self.multisubaxis is not None:
2787 self.subaxis.append(self.createsubaxis())
2788 if (not self.fixnames or name in self.names) and len(subnames):
2789 if self.multisubaxis is not None:
2790 if self.subaxis[self.names.index(name)].setname(*subnames):
2791 self.relsizes = None
2792 else:
2793 if self.subaxis.setname(*subnames):
2794 self.relsizes = None
2795 return self.relsizes is not None
2797 def updaterelsizes(self):
2798 # guess what it does: it recalculates relsize attribute
2799 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
2800 self.relsizes[-1] += self.lastdist - self.dist
2801 if self.multisubaxis is not None:
2802 subrelsize = 0
2803 for i in range(1, len(self.relsizes)):
2804 self.subaxis[i-1].updaterelsizes()
2805 subrelsize += self.subaxis[i-1].relsizes[-1]
2806 self.relsizes[i] += subrelsize
2807 else:
2808 if self.subaxis is None:
2809 subrelsize = 1
2810 else:
2811 self.subaxis.updaterelsizes()
2812 subrelsize = self.subaxis.relsizes[-1]
2813 for i in range(1, len(self.relsizes)):
2814 self.relsizes[i] += i * subrelsize
2816 def convert(self, value):
2817 """baraxis convert method
2818 - the value should be a list, where the first entry is
2819 a member of the names (set in the constructor or by the
2820 setname method); this first entry identifies an item in
2821 the baraxis
2822 - following values are passed to the appropriate subaxis
2823 convert method
2824 - when there is no subaxis, the convert method will behave
2825 like having a linaxis from 0 to 1 as subaxis"""
2826 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2827 if not self.relsizes:
2828 self.updaterelsizes()
2829 pos = self.names.index(value[0])
2830 if len(value) == 2:
2831 if self.subaxis is None:
2832 subvalue = value[1]
2833 else:
2834 if self.multisubaxis is not None:
2835 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
2836 else:
2837 subvalue = value[1] * self.subaxis.relsizes[-1]
2838 else:
2839 if self.multisubaxis is not None:
2840 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
2841 else:
2842 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
2843 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
2845 def baseline(self, x1=None, x2=None, axis=None):
2846 "None is returned -> subaxis should not paint any baselines"
2847 # TODO: quick hack ?!
2848 return None
2850 def vbaseline(self, v1=None, v2=None, axis=None):
2851 "None is returned -> subaxis should not paint any baselines"
2852 # TODO: quick hack ?!
2853 return None
2855 def finish(self, axispos, texrunner):
2856 if self.multisubaxis is not None:
2857 for name, subaxis in zip(self.names, self.subaxis):
2858 subaxis.vmin = self.convert((name, 0))
2859 subaxis.vmax = self.convert((name, 1))
2860 subaxis.baseaxispos = axispos
2861 subaxis.baseaxis = self
2862 self.axiscanvas = axiscanvas(texrunner)
2863 self.painter.paint(axispos, self, self.axiscanvas)
2865 def createlinkaxis(self, **args):
2866 return linkbaraxis(self, **args)
2869 class linkbaraxis(linkaxis):
2870 """a baraxis linked to an already existing baraxis
2871 - inherits the access to a linked axis -- as before,
2872 basically only the painter is replaced
2873 - it must take care of the creation of linked axes of
2874 the subaxes"""
2876 __implements__ = _Iaxis
2878 def __init__(self, linkedaxis, painter=linkbaraxispainter()):
2879 """initializes the instance
2880 - it gets a axis this linkaxis is linked to
2881 - it gets a painter to be used for this linked axis"""
2882 linkaxis.__init__(self, linkedaxis, painter=painter)
2884 def finish(self, axispos, texrunner):
2885 if self.multisubaxis is not None:
2886 self.subaxis = [subaxis.createlinkaxis() for subaxis in self.linkedaxis.subaxis]
2887 for subaxis in self.subaxis:
2888 subaxis.baseaxispos = axispos
2889 subaxis.baseaxis = self
2890 elif self.linkedaxis.subaxis is not None:
2891 self.subaxis = self.linkedaxis.subaxis.createlinkaxis()
2892 self.subaxis.baseaxispos = axispos
2893 self.subaxis.baseaxis = self
2894 linkaxis.finish(self, axispos, texrunner)
2897 ################################################################################
2898 # graph key
2899 ################################################################################
2902 # g = graph.graphxy(key=graph.key())
2903 # g.addkey(graph.key(), ...)
2906 class key:
2908 def __init__(self, dist="0.2 cm", pos = "tr", hinside = 1, vinside = 1, hdist="0.6 cm", vdist="0.4 cm",
2909 symbolwidth="0.5 cm", symbolheight="0.25 cm", symbolspace="0.2 cm",
2910 textattrs=textmodule.vshift.mathaxis):
2911 self.dist_str = dist
2912 self.pos = pos
2913 self.hinside = hinside
2914 self.vinside = vinside
2915 self.hdist_str = hdist
2916 self.vdist_str = vdist
2917 self.symbolwidth_str = symbolwidth
2918 self.symbolheight_str = symbolheight
2919 self.symbolspace_str = symbolspace
2920 self.textattrs = textattrs
2921 self.plotinfos = None
2922 if self.pos in ("tr", "rt"):
2923 self.right = 1
2924 self.top = 1
2925 elif self.pos in ("br", "rb"):
2926 self.right = 1
2927 self.top = 0
2928 elif self.pos in ("tl", "lt"):
2929 self.right = 0
2930 self.top = 1
2931 elif self.pos in ("bl", "lb"):
2932 self.right = 0
2933 self.top = 0
2934 else:
2935 raise RuntimeError("invalid pos attribute")
2937 def setplotinfos(self, *plotinfos):
2938 """set the plotinfos to be used in the key
2939 - call it exactly once"""
2940 if self.plotinfos is not None:
2941 raise RuntimeError("setplotinfo is called multiple times")
2942 self.plotinfos = plotinfos
2944 def dolayout(self, graph):
2945 "creates the layout of the key"
2946 self._dist = unit.topt(unit.length(self.dist_str, default_type="v"))
2947 self._hdist = unit.topt(unit.length(self.hdist_str, default_type="v"))
2948 self._vdist = unit.topt(unit.length(self.vdist_str, default_type="v"))
2949 self._symbolwidth = unit.topt(unit.length(self.symbolwidth_str, default_type="v"))
2950 self._symbolheight = unit.topt(unit.length(self.symbolheight_str, default_type="v"))
2951 self._symbolspace = unit.topt(unit.length(self.symbolspace_str, default_type="v"))
2952 self.titles = []
2953 for plotinfo in self.plotinfos:
2954 self.titles.append(graph.texrunner._text(0, 0, plotinfo.data.title, *helper.ensuresequence(self.textattrs)))
2955 box._tile(self.titles, self._dist, 0, -1)
2956 box._linealignequal(self.titles, self._symbolwidth + self._symbolspace, 1, 0)
2958 def bbox(self):
2959 """return a bbox for the key
2960 method should be called after dolayout"""
2961 result = self.titles[0].bbox()
2962 for title in self.titles[1:]:
2963 result = result + title.bbox() + bbox._bbox(0, title.center[1] - 0.5 * self._symbolheight,
2964 0, title.center[1] + 0.5 * self._symbolheight)
2965 return result
2967 def paint(self, c, x, y):
2968 """paint the graph key into a canvas c at the position x and y (in postscript points)
2969 - method should be called after dolayout
2970 - the x, y alignment might be calculated by the graph using:
2971 - the bbox of the key as returned by the keys bbox method
2972 - the attributes _hdist, _vdist, hinside, and vinside of the key
2973 - the dimension and geometry of the graph"""
2974 sc = c.insert(canvas.canvas(trafomodule._translate(x, y)))
2975 for plotinfo, title in zip(self.plotinfos, self.titles):
2976 plotinfo.style.key(sc, 0, -0.5 * self._symbolheight + title.center[1],
2977 self._symbolwidth, self._symbolheight)
2978 sc.insert(title)
2981 ################################################################################
2982 # graph
2983 ################################################################################
2986 class plotinfo:
2988 def __init__(self, data, style):
2989 self.data = data
2990 self.style = style
2994 class graphxy(canvas.canvas):
2996 Names = "x", "y"
2998 class axisposdata:
3000 def __init__(self, type, axispos, tickdirection):
3002 - type == 0: x-axis; type == 1: y-axis
3003 - _axispos is the y or x position of the x-axis or y-axis
3004 in postscript points, respectively
3005 - axispos is analogous to _axispos, but as a PyX length
3006 - dx and dy is the tick direction
3008 self.type = type
3009 self.axispos = axispos
3010 self._axispos = unit.topt(axispos)
3011 self.tickdirection = tickdirection
3013 def clipcanvas(self):
3014 return self.insert(canvas.canvas(canvas.clip(path._rect(self._xpos, self._ypos, self._width, self._height))))
3016 def plot(self, data, style=None):
3017 if self.haslayout:
3018 raise RuntimeError("layout setup was already performed")
3019 if style is None:
3020 if helper.issequence(data):
3021 raise RuntimeError("list plot needs an explicit style")
3022 if self.defaultstyle.has_key(data.defaultstyle):
3023 style = self.defaultstyle[data.defaultstyle].iterate()
3024 else:
3025 style = data.defaultstyle()
3026 self.defaultstyle[data.defaultstyle] = style
3027 plotinfos = []
3028 first = 1
3029 for d in helper.ensuresequence(data):
3030 if not first:
3031 style = style.iterate()
3032 first = 0
3033 if d is not None:
3034 d.setstyle(self, style)
3035 plotinfos.append(plotinfo(d, style))
3036 self.plotinfos.extend(plotinfos)
3037 if helper.issequence(data):
3038 return plotinfos
3039 return plotinfos[0]
3041 def addkey(self, key, *plotinfos):
3042 if self.haslayout:
3043 raise RuntimeError("layout setup was already performed")
3044 self.addkeys.append((key, plotinfos))
3046 def _pos(self, x, y, xaxis=None, yaxis=None):
3047 if xaxis is None:
3048 xaxis = self.axes["x"]
3049 if yaxis is None:
3050 yaxis = self.axes["y"]
3051 return self._xpos+xaxis.convert(x)*self._width, self._ypos+yaxis.convert(y)*self._height
3053 def pos(self, x, y, xaxis=None, yaxis=None):
3054 if xaxis is None:
3055 xaxis = self.axes["x"]
3056 if yaxis is None:
3057 yaxis = self.axes["y"]
3058 return self.xpos+xaxis.convert(x)*self.width, self.ypos+yaxis.convert(y)*self.height
3060 def _vpos(self, vx, vy, xaxis=None, yaxis=None):
3061 if xaxis is None:
3062 xaxis = self.axes["x"]
3063 if yaxis is None:
3064 yaxis = self.axes["y"]
3065 return self._xpos+vx*self._width, self._ypos+vy*self._height
3067 def vpos(self, vx, vy, xaxis=None, yaxis=None):
3068 if xaxis is None:
3069 xaxis = self.axes["x"]
3070 if yaxis is None:
3071 yaxis = self.axes["y"]
3072 return self.xpos+vx*self.width, self.ypos+vy*self.height
3074 def baseline(self, x1=None, x2=None, axis=None):
3075 if x1 is not None:
3076 v1 = axis.convert(x1)
3077 else:
3078 v1 = 0
3079 if x2 is not None:
3080 v2 = axis.convert(x2)
3081 else:
3082 v2 = 1
3083 if axis.axisposdata.type:
3084 return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3085 axis.axisposdata._axispos, self._ypos+v2*self._height)
3086 else:
3087 return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3088 self._xpos+v2*self._width, axis.axisposdata._axispos)
3090 def xbaseline(self, x1=None, x2=None, axis=None):
3091 if axis is None:
3092 axis = self.axes["x"]
3093 if axis.axisposdata.type != 0:
3094 raise RuntimeError("wrong axisposdata.type")
3095 if x1 is not None:
3096 v1 = axis.convert(x1)
3097 else:
3098 v1 = 0
3099 if x2 is not None:
3100 v2 = axis.convert(x2)
3101 else:
3102 v2 = 1
3103 return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3104 self._xpos+v2*self._width, axis.axisposdata._axispos)
3106 def ybaseline(self, x1=None, x2=None, axis=None):
3107 if axis is None:
3108 axis = self.axes["y"]
3109 if axis.axisposdata.type != 1:
3110 raise RuntimeError("wrong axisposdata.type")
3111 if x1 is not None:
3112 v1 = axis.convert(x1)
3113 else:
3114 v1 = 0
3115 if x2 is not None:
3116 v2 = axis.convert(x2)
3117 else:
3118 v2 = 1
3119 return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3120 axis.axisposdata._axispos, self._ypos+v2*self._height)
3122 def vbaseline(self, v1=None, v2=None, axis=None):
3123 if v1 is None:
3124 v1 = 0
3125 if v2 is None:
3126 v2 = 1
3127 if axis.axisposdata.type:
3128 return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3129 axis.axisposdata._axispos, self._ypos+v2*self._height)
3130 else:
3131 return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3132 self._xpos+v2*self._width, axis.axisposdata._axispos)
3134 def vxbaseline(self, v1=None, v2=None, axis=None):
3135 if axis is None:
3136 axis = self.axes["x"]
3137 if axis.axisposdata.type != 0:
3138 raise RuntimeError("wrong axisposdata.type")
3139 if v1 is None:
3140 v1 = 0
3141 if v2 is None:
3142 v2 = 1
3143 return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3144 self._xpos+v2*self._width, axis.axisposdata._axispos)
3146 def vybaseline(self, v1=None, v2=None, axis=None):
3147 if axis is None:
3148 axis = self.axes["y"]
3149 if axis.axisposdata.type != 1:
3150 raise RuntimeError("wrong axisposdata.type")
3151 if v1 is None:
3152 v1 = 0
3153 if v2 is None:
3154 v2 = 1
3155 return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3156 axis.axisposdata._axispos, self._ypos+v2*self._height)
3158 def gridline(self, x, axis=None):
3159 v = axis.convert(x)
3160 if axis.axisposdata.type:
3161 return path._line(self._xpos, self._ypos+v*self._height,
3162 self._xpos+self._width, self._ypos+v*self._height)
3163 else:
3164 return path._line(self._xpos+v*self._width, self._ypos,
3165 self._xpos+v*self._width, self._ypos+self._height)
3167 def xgridline(self, x, axis=None):
3168 if axis is None:
3169 axis = self.axes["x"]
3170 v = axis.convert(x)
3171 if axis.axisposdata.type != 0:
3172 raise RuntimeError("wrong axisposdata.type")
3173 return path._line(self._xpos+v*self._width, self._ypos,
3174 self._xpos+v*self._width, self._ypos+self._height)
3176 def ygridline(self, x, axis=None):
3177 if axis is None:
3178 axis = self.axes["y"]
3179 v = axis.convert(x)
3180 if axis.axisposdata.type != 1:
3181 raise RuntimeError("wrong axisposdata.type")
3182 return path._line(self._xpos, self._ypos+v*self._height,
3183 self._xpos+self._width, self._ypos+v*self._height)
3185 def vgridline(self, v, axis=None):
3186 if axis.axisposdata.type:
3187 return path._line(self._xpos, self._ypos+v*self._height,
3188 self._xpos+self._width, self._ypos+v*self._height)
3189 else:
3190 return path._line(self._xpos+v*self._width, self._ypos,
3191 self._xpos+v*self._width, self._ypos+self._height)
3193 def vxgridline(self, v, axis=None):
3194 if axis is None:
3195 axis = self.axes["x"]
3196 if axis.axisposdata.type != 0:
3197 raise RuntimeError("wrong axisposdata.type")
3198 return path._line(self._xpos+v*self._width, self._ypos,
3199 self._xpos+v*self._width, self._ypos+self._height)
3201 def vygridline(self, v, axis=None):
3202 if axis is None:
3203 axis = self.axes["y"]
3204 if axis.axisposdata.type != 1:
3205 raise RuntimeError("wrong axisposdata.type")
3206 return path._line(self._xpos, self._ypos+v*self._height,
3207 self._xpos+self._width, self._ypos+v*self._height)
3209 def _tickpoint(self, x, axis=None):
3210 if axis.axisposdata.type:
3211 return axis.axisposdata._axispos, self._ypos+axis.convert(x)*self._height
3212 else:
3213 return self._xpos+axis.convert(x)*self._width, axis.axisposdata._axispos
3215 def _xtickpoint(self, x, axis=None):
3216 if axis is None:
3217 axis = self.axes["x"]
3218 if axis.axisposdata.type != 0:
3219 raise RuntimeError("wrong axisposdata.type")
3220 return self._xpos+axis.convert(x)*self._width, axis.axisposdata._axispos
3222 def _ytickpoint(self, x, axis=None):
3223 if axis is None:
3224 axis = self.axes["y"]
3225 if axis.axisposdata.type != 1:
3226 raise RuntimeError("wrong axisposdata.type")
3227 return axis.axisposdata._axispos, self._ypos+axis.convert(x)*self._height
3229 def tickpoint(self, x, axis=None):
3230 if axis.axisposdata.type:
3231 return axis.axisposdata.axispos, self.ypos+axis.convert(x)*self.height
3232 else:
3233 return self.xpos+axis.convert(x)*self.width, axis.axisposdata.axispos
3235 def xtickpoint(self, x, axis=None):
3236 if axis is None:
3237 axis = self.axes["x"]
3238 if axis.axisposdata.type != 0:
3239 raise RuntimeError("wrong axisposdata.type")
3240 return self.xpos+axis.convert(x)*self.width, axis.axisposdata.axispos
3242 def ytickpoint(self, x, axis=None):
3243 if axis is None:
3244 axis = self.axes["y"]
3245 if axis.axisposdata.type != 1:
3246 raise RuntimeError("wrong axisposdata.type")
3247 return axis.axisposdata.axispos, self.ypos+axis.convert(x)*self.height
3249 def _vtickpoint(self, v, axis=None):
3250 if axis.axisposdata.type:
3251 return axis.axisposdata._axispos, self._ypos+v*self._height
3252 else:
3253 return self._xpos+v*self._width, axis.axisposdata._axispos
3255 def _vxtickpoint(self, v, axis=None):
3256 if axis is None:
3257 axis = self.axes["x"]
3258 if axis.axisposdata.type != 0:
3259 raise RuntimeError("wrong axisposdata.type")
3260 return self._xpos+v*self._width, axis.axisposdata._axispos
3262 def _vytickpoint(self, v, axis=None):
3263 if axis is None:
3264 axis = self.axes["y"]
3265 if axis.axisposdata.type != 1:
3266 raise RuntimeError("wrong axisposdata.type")
3267 return axis.axisposdata._axispos, self._ypos+v*self._height
3269 def vtickpoint(self, v, axis=None):
3270 if axis.axisposdata.type:
3271 return axis.axisposdata.axispos, self.ypos+v*self.height
3272 else:
3273 return self.xpos+v*self.width, axis.axisposdata.axispos
3275 def vxtickpoint(self, v, axis=None):
3276 if axis is None:
3277 axis = self.axes["x"]
3278 if axis.axisposdata.type != 0:
3279 raise RuntimeError("wrong axisposdata.type")
3280 return self.xpos+v*self.width, axis.axisposdata.axispos
3282 def vytickpoint(self, v, axis=None):
3283 if axis is None:
3284 axis = self.axes["y"]
3285 if axis.axisposdata.type != 1:
3286 raise RuntimeError("wrong axisposdata.type")
3287 return axis.axisposdata.axispos, self.ypos+v*self.height
3289 def tickdirection(self, x, axis=None):
3290 return axis.axisposdata.tickdirection
3292 def xtickdirection(self, x, axis=None):
3293 if axis is None:
3294 axis = self.axes["x"]
3295 if axis.axisposdata.type != 0:
3296 raise RuntimeError("wrong axisposdata.type")
3297 return axis.axisposdata.tickdirection
3299 def ytickdirection(self, x, axis=None):
3300 if axis is None:
3301 axis = self.axes["y"]
3302 if axis.axisposdata.type != 1:
3303 raise RuntimeError("wrong axisposdata.type")
3304 return axis.axisposdata.tickdirection
3306 def vtickdirection(self, v, axis=None):
3307 return axis.axisposdata.tickdirection
3309 def vxtickdirection(self, v, axis=None):
3310 if axis is None:
3311 axis = self.axes["x"]
3312 if axis.axisposdata.type != 0:
3313 raise RuntimeError("wrong axisposdata.type")
3314 return axis.axisposdata.tickdirection
3316 def vytickdirection(self, v, axis=None):
3317 if axis is None:
3318 axis = self.axes["y"]
3319 if axis.axisposdata.type != 1:
3320 raise RuntimeError("wrong axisposdata.type")
3321 return axis.axisposdata.tickdirection
3323 def _addpos(self, x, y, dx, dy):
3324 return x+dx, y+dy
3326 def _connect(self, x1, y1, x2, y2):
3327 return path._lineto(x2, y2)
3329 def keynum(self, key):
3330 try:
3331 while key[0] in string.letters:
3332 key = key[1:]
3333 return int(key)
3334 except IndexError:
3335 return 1
3337 def gatherranges(self):
3338 ranges = {}
3339 for plotinfo in self.plotinfos:
3340 pdranges = plotinfo.data.getranges()
3341 if pdranges is not None:
3342 for key in pdranges.keys():
3343 if key not in ranges.keys():
3344 ranges[key] = pdranges[key]
3345 else:
3346 ranges[key] = (min(ranges[key][0], pdranges[key][0]),
3347 max(ranges[key][1], pdranges[key][1]))
3348 # known ranges are also set as ranges for the axes
3349 for key, axis in self.axes.items():
3350 if key in ranges.keys():
3351 axis.setdatarange(*ranges[key])
3352 ranges[key] = axis.getdatarange()
3353 if ranges[key] is None:
3354 del ranges[key]
3355 return ranges
3357 def removedomethod(self, method):
3358 hadmethod = 0
3359 while 1:
3360 try:
3361 self.domethods.remove(method)
3362 hadmethod = 1
3363 except ValueError:
3364 return hadmethod
3366 def dolayout(self):
3367 if not self.removedomethod(self.dolayout): return
3368 self.haslayout = 1
3369 # create list of ranges
3370 # 1. gather ranges
3371 ranges = self.gatherranges()
3372 # 2. calculate additional ranges out of known ranges
3373 for plotinfo in self.plotinfos:
3374 plotinfo.data.setranges(ranges)
3375 # 3. gather ranges again
3376 self.gatherranges()
3377 # do the layout for all axes
3378 axesdist = unit.length(self.axesdist_str, default_type="v")
3379 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3380 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3381 xaxisextents = [0, 0]
3382 yaxisextents = [0, 0]
3383 needxaxisdist = [0, 0]
3384 needyaxisdist = [0, 0]
3385 items = list(self.axes.items())
3386 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3387 for key, axis in items:
3388 num = self.keynum(key)
3389 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3390 num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3391 if XPattern.match(key):
3392 if needxaxisdist[num2]:
3393 xaxisextents[num2] += axesdist
3394 axis.axisposdata = self.axisposdata(0, self.ypos + num2*self.height + num3*xaxisextents[num2], (0, num3))
3395 elif YPattern.match(key):
3396 if needyaxisdist[num2]:
3397 yaxisextents[num2] += axesdist
3398 axis.axisposdata = self.axisposdata(1, self.xpos + num2*self.width + num3*yaxisextents[num2], (num3, 0))
3399 else:
3400 raise ValueError("Axis key '%s' not allowed" % key)
3401 axis.finish(self, self.texrunner)
3402 if XPattern.match(key):
3403 xaxisextents[num2] += axis.axiscanvas.extent
3404 needxaxisdist[num2] = 1
3405 if YPattern.match(key):
3406 yaxisextents[num2] += axis.axiscanvas.extent
3407 needyaxisdist[num2] = 1
3409 def dobackground(self):
3410 self.dolayout()
3411 if not self.removedomethod(self.dobackground): return
3412 if self.backgroundattrs is not None:
3413 self.draw(path._rect(self._xpos, self._ypos, self._width, self._height),
3414 *helper.ensuresequence(self.backgroundattrs))
3416 def doaxes(self):
3417 self.dolayout()
3418 if not self.removedomethod(self.doaxes): return
3419 for axis in self.axes.values():
3420 self.insert(axis.axiscanvas)
3422 def dodata(self):
3423 self.dolayout()
3424 if not self.removedomethod(self.dodata): return
3425 for plotinfo in self.plotinfos:
3426 plotinfo.data.draw(self)
3428 def _dokey(self, key, *plotinfos):
3429 key.setplotinfos(*plotinfos)
3430 key.dolayout(self)
3431 bbox = key.bbox()
3432 if key.right:
3433 if key.hinside:
3434 x = self._xpos + self._width - bbox.urx - key._hdist
3435 else:
3436 x = self._xpos + self._width - bbox.llx + key._hdist
3437 else:
3438 if key.hinside:
3439 x = self._xpos - bbox.llx + key._hdist
3440 else:
3441 x = self._xpos - bbox.urx - key._hdist
3442 if key.top:
3443 if key.vinside:
3444 y = self._ypos + self._height - bbox.ury - key._vdist
3445 else:
3446 y = self._ypos + self._height - bbox.lly + key._vdist
3447 else:
3448 if key.vinside:
3449 y = self._ypos - bbox.lly + key._vdist
3450 else:
3451 y = self._ypos - bbox.ury - key._vdist
3452 key.paint(self, x, y)
3454 def dokey(self):
3455 self.dolayout()
3456 if not self.removedomethod(self.dokey): return
3457 if self.key is not None:
3458 self._dokey(self.key, *self.plotinfos)
3459 for key, plotinfos in self.addkeys:
3460 self._dokey(key, *plotinfos)
3462 def finish(self):
3463 while len(self.domethods):
3464 self.domethods[0]()
3466 def initwidthheight(self, width, height, ratio):
3467 if (width is not None) and (height is None):
3468 self.width = unit.length(width)
3469 self.height = (1.0/ratio) * self.width
3470 elif (height is not None) and (width is None):
3471 self.height = unit.length(height)
3472 self.width = ratio * self.height
3473 else:
3474 self.width = unit.length(width)
3475 self.height = unit.length(height)
3476 self._width = unit.topt(self.width)
3477 self._height = unit.topt(self.height)
3478 if self._width <= 0: raise ValueError("width <= 0")
3479 if self._height <= 0: raise ValueError("height <= 0")
3481 def initaxes(self, axes, addlinkaxes=0):
3482 for key in self.Names:
3483 if not axes.has_key(key):
3484 axes[key] = linaxis()
3485 elif axes[key] is None:
3486 del axes[key]
3487 if addlinkaxes:
3488 if not axes.has_key(key + "2") and axes.has_key(key):
3489 axes[key + "2"] = axes[key].createlinkaxis()
3490 elif axes[key + "2"] is None:
3491 del axes[key + "2"]
3492 self.axes = axes
3494 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
3495 key=None, backgroundattrs=None, axesdist="0.8 cm", **axes):
3496 canvas.canvas.__init__(self)
3497 self.xpos = unit.length(xpos)
3498 self.ypos = unit.length(ypos)
3499 self._xpos = unit.topt(self.xpos)
3500 self._ypos = unit.topt(self.ypos)
3501 self.initwidthheight(width, height, ratio)
3502 self.initaxes(axes, 1)
3503 self.key = key
3504 self.backgroundattrs = backgroundattrs
3505 self.axesdist_str = axesdist
3506 self.plotinfos = []
3507 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata, self.dokey]
3508 self.haslayout = 0
3509 self.defaultstyle = {}
3510 self.addkeys = []
3512 def bbox(self):
3513 self.finish()
3514 return canvas.canvas.bbox(self)
3516 def write(self, file):
3517 self.finish()
3518 canvas.canvas.write(self, file)
3522 # some thoughts, but deferred right now
3524 # class graphxyz(graphxy):
3526 # Names = "x", "y", "z"
3528 # def _vxtickpoint(self, axis, v):
3529 # return self._vpos(v, axis.vypos, axis.vzpos)
3531 # def _vytickpoint(self, axis, v):
3532 # return self._vpos(axis.vxpos, v, axis.vzpos)
3534 # def _vztickpoint(self, axis, v):
3535 # return self._vpos(axis.vxpos, axis.vypos, v)
3537 # def vxtickdirection(self, axis, v):
3538 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
3539 # x2, y2 = self._vpos(v, 0.5, 0)
3540 # dx, dy = x1 - x2, y1 - y2
3541 # norm = math.sqrt(dx*dx + dy*dy)
3542 # return dx/norm, dy/norm
3544 # def vytickdirection(self, axis, v):
3545 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
3546 # x2, y2 = self._vpos(0.5, v, 0)
3547 # dx, dy = x1 - x2, y1 - y2
3548 # norm = math.sqrt(dx*dx + dy*dy)
3549 # return dx/norm, dy/norm
3551 # def vztickdirection(self, axis, v):
3552 # return -1, 0
3553 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
3554 # x2, y2 = self._vpos(0.5, 0.5, v)
3555 # dx, dy = x1 - x2, y1 - y2
3556 # norm = math.sqrt(dx*dx + dy*dy)
3557 # return dx/norm, dy/norm
3559 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3560 # if xaxis is None: xaxis = self.axes["x"]
3561 # if yaxis is None: yaxis = self.axes["y"]
3562 # if zaxis is None: zaxis = self.axes["z"]
3563 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3565 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3566 # if xaxis is None: xaxis = self.axes["x"]
3567 # if yaxis is None: yaxis = self.axes["y"]
3568 # if zaxis is None: zaxis = self.axes["z"]
3569 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3571 # def _vpos(self, vx, vy, vz):
3572 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
3573 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
3574 # + self.a[2]*self.b[0]*(y-self.eye[1])
3575 # + self.a[1]*self.b[2]*(x-self.eye[0])
3576 # - self.a[2]*self.b[1]*(x-self.eye[0])
3577 # - self.a[0]*self.b[2]*(y-self.eye[1])
3578 # - self.a[1]*self.b[0]*(z-self.eye[2]))
3579 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
3580 # + self.eye[2]*self.b[0]*(y-self.eye[1])
3581 # + self.eye[1]*self.b[2]*(x-self.eye[0])
3582 # - self.eye[2]*self.b[1]*(x-self.eye[0])
3583 # - self.eye[0]*self.b[2]*(y-self.eye[1])
3584 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
3585 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
3586 # + self.a[2]*self.eye[0]*(y-self.eye[1])
3587 # + self.a[1]*self.eye[2]*(x-self.eye[0])
3588 # - self.a[2]*self.eye[1]*(x-self.eye[0])
3589 # - self.a[0]*self.eye[2]*(y-self.eye[1])
3590 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
3591 # return da/d0 + self._xpos, db/d0 + self._ypos
3593 # def vpos(self, vx, vy, vz):
3594 # tx, ty = self._vpos(vx, vy, vz)
3595 # return unit.t_pt(tx), unit.t_pt(ty)
3597 # def xbaseline(self, axis, x1, x2, xaxis=None):
3598 # if xaxis is None: xaxis = self.axes["x"]
3599 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2))
3601 # def ybaseline(self, axis, y1, y2, yaxis=None):
3602 # if yaxis is None: yaxis = self.axes["y"]
3603 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2))
3605 # def zbaseline(self, axis, z1, z2, zaxis=None):
3606 # if zaxis is None: zaxis = self.axes["z"]
3607 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2))
3609 # def vxbaseline(self, axis, v1, v2):
3610 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
3611 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
3612 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
3613 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
3615 # def vybaseline(self, axis, v1, v2):
3616 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
3617 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
3618 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
3619 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
3621 # def vzbaseline(self, axis, v1, v2):
3622 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
3623 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
3624 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
3625 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
3627 # def xgridpath(self, x, xaxis=None):
3628 # assert 0
3629 # if xaxis is None: xaxis = self.axes["x"]
3630 # v = xaxis.convert(x)
3631 # return path._line(self._xpos+v*self._width, self._ypos,
3632 # self._xpos+v*self._width, self._ypos+self._height)
3634 # def ygridpath(self, y, yaxis=None):
3635 # assert 0
3636 # if yaxis is None: yaxis = self.axes["y"]
3637 # v = yaxis.convert(y)
3638 # return path._line(self._xpos, self._ypos+v*self._height,
3639 # self._xpos+self._width, self._ypos+v*self._height)
3641 # def zgridpath(self, z, zaxis=None):
3642 # assert 0
3643 # if zaxis is None: zaxis = self.axes["z"]
3644 # v = zaxis.convert(z)
3645 # return path._line(self._xpos, self._zpos+v*self._height,
3646 # self._xpos+self._width, self._zpos+v*self._height)
3648 # def vxgridpath(self, v):
3649 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
3650 # path._lineto(*self._vpos(v, 0, 1)),
3651 # path._lineto(*self._vpos(v, 1, 1)),
3652 # path._lineto(*self._vpos(v, 1, 0)),
3653 # path.closepath())
3655 # def vygridpath(self, v):
3656 # return path.path(path._moveto(*self._vpos(0, v, 0)),
3657 # path._lineto(*self._vpos(0, v, 1)),
3658 # path._lineto(*self._vpos(1, v, 1)),
3659 # path._lineto(*self._vpos(1, v, 0)),
3660 # path.closepath())
3662 # def vzgridpath(self, v):
3663 # return path.path(path._moveto(*self._vpos(0, 0, v)),
3664 # path._lineto(*self._vpos(0, 1, v)),
3665 # path._lineto(*self._vpos(1, 1, v)),
3666 # path._lineto(*self._vpos(1, 0, v)),
3667 # path.closepath())
3669 # def _addpos(self, x, y, dx, dy):
3670 # assert 0
3671 # return x+dx, y+dy
3673 # def _connect(self, x1, y1, x2, y2):
3674 # assert 0
3675 # return path._lineto(x2, y2)
3677 # def doaxes(self):
3678 # self.dolayout()
3679 # if not self.removedomethod(self.doaxes): return
3680 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
3681 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3682 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3683 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[2])
3684 # items = list(self.axes.items())
3685 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3686 # for key, axis in items:
3687 # num = self.keynum(key)
3688 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3689 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3690 # if XPattern.match(key):
3691 # axis.vypos = 0
3692 # axis.vzpos = 0
3693 # axis._vtickpoint = self._vxtickpoint
3694 # axis.vgridpath = self.vxgridpath
3695 # axis.vbaseline = self.vxbaseline
3696 # axis.vtickdirection = self.vxtickdirection
3697 # elif YPattern.match(key):
3698 # axis.vxpos = 0
3699 # axis.vzpos = 0
3700 # axis._vtickpoint = self._vytickpoint
3701 # axis.vgridpath = self.vygridpath
3702 # axis.vbaseline = self.vybaseline
3703 # axis.vtickdirection = self.vytickdirection
3704 # elif ZPattern.match(key):
3705 # axis.vxpos = 0
3706 # axis.vypos = 0
3707 # axis._vtickpoint = self._vztickpoint
3708 # axis.vgridpath = self.vzgridpath
3709 # axis.vbaseline = self.vzbaseline
3710 # axis.vtickdirection = self.vztickdirection
3711 # else:
3712 # raise ValueError("Axis key '%s' not allowed" % key)
3713 # if axis.painter is not None:
3714 # axis.dopaint(self)
3715 # # if XPattern.match(key):
3716 # # self._xaxisextents[num2] += axis._extent
3717 # # needxaxisdist[num2] = 1
3718 # # if YPattern.match(key):
3719 # # self._yaxisextents[num2] += axis._extent
3720 # # needyaxisdist[num2] = 1
3722 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
3723 # phi=30, theta=30, distance=1,
3724 # backgroundattrs=None, axesdist="0.8 cm", **axes):
3725 # canvas.canvas.__init__(self)
3726 # self.tex = tex
3727 # self.xpos = xpos
3728 # self.ypos = ypos
3729 # self._xpos = unit.topt(xpos)
3730 # self._ypos = unit.topt(ypos)
3731 # self._width = unit.topt(width)
3732 # self._height = unit.topt(height)
3733 # self._depth = unit.topt(depth)
3734 # self.width = width
3735 # self.height = height
3736 # self.depth = depth
3737 # if self._width <= 0: raise ValueError("width < 0")
3738 # if self._height <= 0: raise ValueError("height < 0")
3739 # if self._depth <= 0: raise ValueError("height < 0")
3740 # self._distance = distance*math.sqrt(self._width*self._width+
3741 # self._height*self._height+
3742 # self._depth*self._depth)
3743 # phi *= -math.pi/180
3744 # theta *= math.pi/180
3745 # self.a = (-math.sin(phi), math.cos(phi), 0)
3746 # self.b = (-math.cos(phi)*math.sin(theta),
3747 # -math.sin(phi)*math.sin(theta),
3748 # math.cos(theta))
3749 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
3750 # self._distance*math.sin(phi)*math.cos(theta),
3751 # self._distance*math.sin(theta))
3752 # self.initaxes(axes)
3753 # self.axesdist_str = axesdist
3754 # self.backgroundattrs = backgroundattrs
3756 # self.data = []
3757 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
3758 # self.haslayout = 0
3759 # self.defaultstyle = {}
3761 # def bbox(self):
3762 # self.finish()
3763 # return bbox._bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
3766 ################################################################################
3767 # attr changers
3768 ################################################################################
3771 #class _Ichangeattr:
3772 # """attribute changer
3773 # is an iterator for attributes where an attribute
3774 # is not refered by just a number (like for a sequence),
3775 # but also by the number of attributes requested
3776 # by calls of the next method (like for an color palette)
3777 # (you should ensure to call all needed next before the attr)
3779 # the attribute itself is implemented by overloading the _attr method"""
3781 # def attr(self):
3782 # "get an attribute"
3784 # def next(self):
3785 # "get an attribute changer for the next attribute"
3788 class _changeattr: pass
3791 class changeattr(_changeattr):
3793 def __init__(self):
3794 self.counter = 1
3796 def getattr(self):
3797 return self.attr(0)
3799 def iterate(self):
3800 newindex = self.counter
3801 self.counter += 1
3802 return refattr(self, newindex)
3805 class refattr(_changeattr):
3807 def __init__(self, ref, index):
3808 self.ref = ref
3809 self.index = index
3811 def getattr(self):
3812 return self.ref.attr(self.index)
3814 def iterate(self):
3815 return self.ref.iterate()
3818 # helper routines for a using attrs
3820 def _getattr(attr):
3821 "get attr out of a attr/changeattr"
3822 if isinstance(attr, _changeattr):
3823 return attr.getattr()
3824 return attr
3827 def _getattrs(attrs):
3828 "get attrs out of a sequence of attr/changeattr"
3829 if attrs is not None:
3830 result = []
3831 for attr in helper.ensuresequence(attrs):
3832 if isinstance(attr, _changeattr):
3833 attr = attr.getattr()
3834 if attr is not None:
3835 result.append(attr)
3836 if len(result) or not len(attrs):
3837 return result
3840 def _iterateattr(attr):
3841 "perform next to a attr/changeattr"
3842 if isinstance(attr, _changeattr):
3843 return attr.iterate()
3844 return attr
3847 def _iterateattrs(attrs):
3848 "perform next to a sequence of attr/changeattr"
3849 if attrs is not None:
3850 result = []
3851 for attr in helper.ensuresequence(attrs):
3852 if isinstance(attr, _changeattr):
3853 result.append(attr.iterate())
3854 else:
3855 result.append(attr)
3856 return result
3859 class changecolor(changeattr):
3861 def __init__(self, palette):
3862 changeattr.__init__(self)
3863 self.palette = palette
3865 def attr(self, index):
3866 if self.counter != 1:
3867 return self.palette.getcolor(index/float(self.counter-1))
3868 else:
3869 return self.palette.getcolor(0)
3872 class _changecolorgray(changecolor):
3874 def __init__(self, palette=color.palette.Gray):
3875 changecolor.__init__(self, palette)
3877 _changecolorgrey = _changecolorgray
3880 class _changecolorreversegray(changecolor):
3882 def __init__(self, palette=color.palette.ReverseGray):
3883 changecolor.__init__(self, palette)
3885 _changecolorreversegrey = _changecolorreversegray
3888 class _changecolorredblack(changecolor):
3890 def __init__(self, palette=color.palette.RedBlack):
3891 changecolor.__init__(self, palette)
3894 class _changecolorblackred(changecolor):
3896 def __init__(self, palette=color.palette.BlackRed):
3897 changecolor.__init__(self, palette)
3900 class _changecolorredwhite(changecolor):
3902 def __init__(self, palette=color.palette.RedWhite):
3903 changecolor.__init__(self, palette)
3906 class _changecolorwhitered(changecolor):
3908 def __init__(self, palette=color.palette.WhiteRed):
3909 changecolor.__init__(self, palette)
3912 class _changecolorgreenblack(changecolor):
3914 def __init__(self, palette=color.palette.GreenBlack):
3915 changecolor.__init__(self, palette)
3918 class _changecolorblackgreen(changecolor):
3920 def __init__(self, palette=color.palette.BlackGreen):
3921 changecolor.__init__(self, palette)
3924 class _changecolorgreenwhite(changecolor):
3926 def __init__(self, palette=color.palette.GreenWhite):
3927 changecolor.__init__(self, palette)
3930 class _changecolorwhitegreen(changecolor):
3932 def __init__(self, palette=color.palette.WhiteGreen):
3933 changecolor.__init__(self, palette)
3936 class _changecolorblueblack(changecolor):
3938 def __init__(self, palette=color.palette.BlueBlack):
3939 changecolor.__init__(self, palette)
3942 class _changecolorblackblue(changecolor):
3944 def __init__(self, palette=color.palette.BlackBlue):
3945 changecolor.__init__(self, palette)
3948 class _changecolorbluewhite(changecolor):
3950 def __init__(self, palette=color.palette.BlueWhite):
3951 changecolor.__init__(self, palette)
3954 class _changecolorwhiteblue(changecolor):
3956 def __init__(self, palette=color.palette.WhiteBlue):
3957 changecolor.__init__(self, palette)
3960 class _changecolorredgreen(changecolor):
3962 def __init__(self, palette=color.palette.RedGreen):
3963 changecolor.__init__(self, palette)
3966 class _changecolorredblue(changecolor):
3968 def __init__(self, palette=color.palette.RedBlue):
3969 changecolor.__init__(self, palette)
3972 class _changecolorgreenred(changecolor):
3974 def __init__(self, palette=color.palette.GreenRed):
3975 changecolor.__init__(self, palette)
3978 class _changecolorgreenblue(changecolor):
3980 def __init__(self, palette=color.palette.GreenBlue):
3981 changecolor.__init__(self, palette)
3984 class _changecolorbluered(changecolor):
3986 def __init__(self, palette=color.palette.BlueRed):
3987 changecolor.__init__(self, palette)
3990 class _changecolorbluegreen(changecolor):
3992 def __init__(self, palette=color.palette.BlueGreen):
3993 changecolor.__init__(self, palette)
3996 class _changecolorrainbow(changecolor):
3998 def __init__(self, palette=color.palette.Rainbow):
3999 changecolor.__init__(self, palette)
4002 class _changecolorreverserainbow(changecolor):
4004 def __init__(self, palette=color.palette.ReverseRainbow):
4005 changecolor.__init__(self, palette)
4008 class _changecolorhue(changecolor):
4010 def __init__(self, palette=color.palette.Hue):
4011 changecolor.__init__(self, palette)
4014 class _changecolorreversehue(changecolor):
4016 def __init__(self, palette=color.palette.ReverseHue):
4017 changecolor.__init__(self, palette)
4020 changecolor.Gray = _changecolorgray
4021 changecolor.Grey = _changecolorgrey
4022 changecolor.Reversegray = _changecolorreversegray
4023 changecolor.Reversegrey = _changecolorreversegrey
4024 changecolor.RedBlack = _changecolorredblack
4025 changecolor.BlackRed = _changecolorblackred
4026 changecolor.RedWhite = _changecolorredwhite
4027 changecolor.WhiteRed = _changecolorwhitered
4028 changecolor.GreenBlack = _changecolorgreenblack
4029 changecolor.BlackGreen = _changecolorblackgreen
4030 changecolor.GreenWhite = _changecolorgreenwhite
4031 changecolor.WhiteGreen = _changecolorwhitegreen
4032 changecolor.BlueBlack = _changecolorblueblack
4033 changecolor.BlackBlue = _changecolorblackblue
4034 changecolor.BlueWhite = _changecolorbluewhite
4035 changecolor.WhiteBlue = _changecolorwhiteblue
4036 changecolor.RedGreen = _changecolorredgreen
4037 changecolor.RedBlue = _changecolorredblue
4038 changecolor.GreenRed = _changecolorgreenred
4039 changecolor.GreenBlue = _changecolorgreenblue
4040 changecolor.BlueRed = _changecolorbluered
4041 changecolor.BlueGreen = _changecolorbluegreen
4042 changecolor.Rainbow = _changecolorrainbow
4043 changecolor.ReverseRainbow = _changecolorreverserainbow
4044 changecolor.Hue = _changecolorhue
4045 changecolor.ReverseHue = _changecolorreversehue
4048 class changesequence(changeattr):
4049 "cycles through a sequence"
4051 def __init__(self, *sequence):
4052 changeattr.__init__(self)
4053 if not len(sequence):
4054 sequence = self.defaultsequence
4055 self.sequence = sequence
4057 def attr(self, index):
4058 return self.sequence[index % len(self.sequence)]
4061 class changelinestyle(changesequence):
4062 defaultsequence = (canvas.linestyle.solid,
4063 canvas.linestyle.dashed,
4064 canvas.linestyle.dotted,
4065 canvas.linestyle.dashdotted)
4068 class changestrokedfilled(changesequence):
4069 defaultsequence = (canvas.stroked(), canvas.filled())
4072 class changefilledstroked(changesequence):
4073 defaultsequence = (canvas.filled(), canvas.stroked())
4077 ################################################################################
4078 # styles
4079 ################################################################################
4082 class symbol:
4084 def cross(self, x, y):
4085 return (path._moveto(x-0.5*self._size, y-0.5*self._size),
4086 path._lineto(x+0.5*self._size, y+0.5*self._size),
4087 path._moveto(x-0.5*self._size, y+0.5*self._size),
4088 path._lineto(x+0.5*self._size, y-0.5*self._size))
4090 def plus(self, x, y):
4091 return (path._moveto(x-0.707106781*self._size, y),
4092 path._lineto(x+0.707106781*self._size, y),
4093 path._moveto(x, y-0.707106781*self._size),
4094 path._lineto(x, y+0.707106781*self._size))
4096 def square(self, x, y):
4097 return (path._moveto(x-0.5*self._size, y-0.5 * self._size),
4098 path._lineto(x+0.5*self._size, y-0.5 * self._size),
4099 path._lineto(x+0.5*self._size, y+0.5 * self._size),
4100 path._lineto(x-0.5*self._size, y+0.5 * self._size),
4101 path.closepath())
4103 def triangle(self, x, y):
4104 return (path._moveto(x-0.759835685*self._size, y-0.438691337*self._size),
4105 path._lineto(x+0.759835685*self._size, y-0.438691337*self._size),
4106 path._lineto(x, y+0.877382675*self._size),
4107 path.closepath())
4109 def circle(self, x, y):
4110 return (path._arc(x, y, 0.564189583*self._size, 0, 360),
4111 path.closepath())
4113 def diamond(self, x, y):
4114 return (path._moveto(x-0.537284965*self._size, y),
4115 path._lineto(x, y-0.930604859*self._size),
4116 path._lineto(x+0.537284965*self._size, y),
4117 path._lineto(x, y+0.930604859*self._size),
4118 path.closepath())
4120 def __init__(self, symbol=helper.nodefault,
4121 size="0.2 cm", symbolattrs=canvas.stroked(),
4122 errorscale=0.5, errorbarattrs=(),
4123 lineattrs=None):
4124 self.size_str = size
4125 if symbol is helper.nodefault:
4126 self._symbol = changesymbol.cross()
4127 else:
4128 self._symbol = symbol
4129 self._symbolattrs = symbolattrs
4130 self.errorscale = errorscale
4131 self._errorbarattrs = errorbarattrs
4132 self._lineattrs = lineattrs
4134 def iteratedict(self):
4135 result = {}
4136 result["symbol"] = _iterateattr(self._symbol)
4137 result["size"] = _iterateattr(self.size_str)
4138 result["symbolattrs"] = _iterateattrs(self._symbolattrs)
4139 result["errorscale"] = _iterateattr(self.errorscale)
4140 result["errorbarattrs"] = _iterateattrs(self._errorbarattrs)
4141 result["lineattrs"] = _iterateattrs(self._lineattrs)
4142 return result
4144 def iterate(self):
4145 return symbol(**self.iteratedict())
4147 def othercolumnkey(self, key, index):
4148 raise ValueError("unsuitable key '%s'" % key)
4150 def setcolumns(self, graph, columns):
4151 def checkpattern(key, index, pattern, iskey, isindex):
4152 if key is not None:
4153 match = pattern.match(key)
4154 if match:
4155 if isindex is not None: raise ValueError("multiple key specification")
4156 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4157 key = None
4158 iskey = match.groups()[0]
4159 isindex = index
4160 return key, iskey, isindex
4162 self.xi = self.xmini = self.xmaxi = None
4163 self.dxi = self.dxmini = self.dxmaxi = None
4164 self.yi = self.ymini = self.ymaxi = None
4165 self.dyi = self.dymini = self.dymaxi = None
4166 self.xkey = self.ykey = None
4167 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4168 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4169 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4170 XMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
4171 YMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
4172 XMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
4173 YMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
4174 DXPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4175 DYPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4176 DXMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
4177 DYMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
4178 DXMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
4179 DYMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
4180 for key, index in columns.items():
4181 key, self.xkey, self.xi = checkpattern(key, index, XPattern, self.xkey, self.xi)
4182 key, self.ykey, self.yi = checkpattern(key, index, YPattern, self.ykey, self.yi)
4183 key, self.xkey, self.xmini = checkpattern(key, index, XMinPattern, self.xkey, self.xmini)
4184 key, self.ykey, self.ymini = checkpattern(key, index, YMinPattern, self.ykey, self.ymini)
4185 key, self.xkey, self.xmaxi = checkpattern(key, index, XMaxPattern, self.xkey, self.xmaxi)
4186 key, self.ykey, self.ymaxi = checkpattern(key, index, YMaxPattern, self.ykey, self.ymaxi)
4187 key, self.xkey, self.dxi = checkpattern(key, index, DXPattern, self.xkey, self.dxi)
4188 key, self.ykey, self.dyi = checkpattern(key, index, DYPattern, self.ykey, self.dyi)
4189 key, self.xkey, self.dxmini = checkpattern(key, index, DXMinPattern, self.xkey, self.dxmini)
4190 key, self.ykey, self.dymini = checkpattern(key, index, DYMinPattern, self.ykey, self.dymini)
4191 key, self.xkey, self.dxmaxi = checkpattern(key, index, DXMaxPattern, self.xkey, self.dxmaxi)
4192 key, self.ykey, self.dymaxi = checkpattern(key, index, DYMaxPattern, self.ykey, self.dymaxi)
4193 if key is not None:
4194 self.othercolumnkey(key, index)
4195 if None in (self.xkey, self.ykey): raise ValueError("incomplete axis specification")
4196 if (len(filter(None, (self.xmini, self.dxmini, self.dxi))) > 1 or
4197 len(filter(None, (self.ymini, self.dymini, self.dyi))) > 1 or
4198 len(filter(None, (self.xmaxi, self.dxmaxi, self.dxi))) > 1 or
4199 len(filter(None, (self.ymaxi, self.dymaxi, self.dyi))) > 1):
4200 raise ValueError("multiple errorbar definition")
4201 if ((self.xi is None and self.dxi is not None) or
4202 (self.yi is None and self.dyi is not None) or
4203 (self.xi is None and self.dxmini is not None) or
4204 (self.yi is None and self.dymini is not None) or
4205 (self.xi is None and self.dxmaxi is not None) or
4206 (self.yi is None and self.dymaxi is not None)):
4207 raise ValueError("errorbar definition start value missing")
4208 self.xaxis = graph.axes[self.xkey]
4209 self.yaxis = graph.axes[self.ykey]
4211 def minmidmax(self, point, i, mini, maxi, di, dmini, dmaxi):
4212 min = max = mid = None
4213 try:
4214 mid = point[i] + 0.0
4215 except (TypeError, ValueError):
4216 pass
4217 try:
4218 if di is not None: min = point[i] - point[di]
4219 elif dmini is not None: min = point[i] - point[dmini]
4220 elif mini is not None: min = point[mini] + 0.0
4221 except (TypeError, ValueError):
4222 pass
4223 try:
4224 if di is not None: max = point[i] + point[di]
4225 elif dmaxi is not None: max = point[i] + point[dmaxi]
4226 elif maxi is not None: max = point[maxi] + 0.0
4227 except (TypeError, ValueError):
4228 pass
4229 if mid is not None:
4230 if min is not None and min > mid: raise ValueError("minimum error in errorbar")
4231 if max is not None and max < mid: raise ValueError("maximum error in errorbar")
4232 else:
4233 if min is not None and max is not None and min > max: raise ValueError("minimum/maximum error in errorbar")
4234 return min, mid, max
4236 def keyrange(self, points, i, mini, maxi, di, dmini, dmaxi):
4237 allmin = allmax = None
4238 if filter(None, (mini, maxi, di, dmini, dmaxi)) is not None:
4239 for point in points:
4240 min, mid, max = self.minmidmax(point, i, mini, maxi, di, dmini, dmaxi)
4241 if min is not None and (allmin is None or min < allmin): allmin = min
4242 if mid is not None and (allmin is None or mid < allmin): allmin = mid
4243 if mid is not None and (allmax is None or mid > allmax): allmax = mid
4244 if max is not None and (allmax is None or max > allmax): allmax = max
4245 else:
4246 for point in points:
4247 try:
4248 value = point[i] + 0.0
4249 if allmin is None or point[i] < allmin: allmin = point[i]
4250 if allmax is None or point[i] > allmax: allmax = point[i]
4251 except (TypeError, ValueError):
4252 pass
4253 return allmin, allmax
4255 def getranges(self, points):
4256 xmin, xmax = self.keyrange(points, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4257 ymin, ymax = self.keyrange(points, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4258 return {self.xkey: (xmin, xmax), self.ykey: (ymin, ymax)}
4260 def _drawerrorbar(self, graph, topleft, top, topright,
4261 left, center, right,
4262 bottomleft, bottom, bottomright, point=None):
4263 if left is not None:
4264 if right is not None:
4265 left1 = graph._addpos(*(left+(0, -self._errorsize)))
4266 left2 = graph._addpos(*(left+(0, self._errorsize)))
4267 right1 = graph._addpos(*(right+(0, -self._errorsize)))
4268 right2 = graph._addpos(*(right+(0, self._errorsize)))
4269 graph.stroke(path.path(path._moveto(*left1),
4270 graph._connect(*(left1+left2)),
4271 path._moveto(*left),
4272 graph._connect(*(left+right)),
4273 path._moveto(*right1),
4274 graph._connect(*(right1+right2))),
4275 *self.errorbarattrs)
4276 elif center is not None:
4277 left1 = graph._addpos(*(left+(0, -self._errorsize)))
4278 left2 = graph._addpos(*(left+(0, self._errorsize)))
4279 graph.stroke(path.path(path._moveto(*left1),
4280 graph._connect(*(left1+left2)),
4281 path._moveto(*left),
4282 graph._connect(*(left+center))),
4283 *self.errorbarattrs)
4284 else:
4285 left1 = graph._addpos(*(left+(0, -self._errorsize)))
4286 left2 = graph._addpos(*(left+(0, self._errorsize)))
4287 left3 = graph._addpos(*(left+(self._errorsize, 0)))
4288 graph.stroke(path.path(path._moveto(*left1),
4289 graph._connect(*(left1+left2)),
4290 path._moveto(*left),
4291 graph._connect(*(left+left3))),
4292 *self.errorbarattrs)
4293 if right is not None and left is None:
4294 if center is not None:
4295 right1 = graph._addpos(*(right+(0, -self._errorsize)))
4296 right2 = graph._addpos(*(right+(0, self._errorsize)))
4297 graph.stroke(path.path(path._moveto(*right1),
4298 graph._connect(*(right1+right2)),
4299 path._moveto(*right),
4300 graph._connect(*(right+center))),
4301 *self.errorbarattrs)
4302 else:
4303 right1 = graph._addpos(*(right+(0, -self._errorsize)))
4304 right2 = graph._addpos(*(right+(0, self._errorsize)))
4305 right3 = graph._addpos(*(right+(-self._errorsize, 0)))
4306 graph.stroke(path.path(path._moveto(*right1),
4307 graph._connect(*(right1+right2)),
4308 path._moveto(*right),
4309 graph._connect(*(right+right3))),
4310 *self.errorbarattrs)
4312 if bottom is not None:
4313 if top is not None:
4314 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
4315 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
4316 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
4317 top2 = graph._addpos(*(top+(self._errorsize, 0)))
4318 graph.stroke(path.path(path._moveto(*bottom1),
4319 graph._connect(*(bottom1+bottom2)),
4320 path._moveto(*bottom),
4321 graph._connect(*(bottom+top)),
4322 path._moveto(*top1),
4323 graph._connect(*(top1+top2))),
4324 *self.errorbarattrs)
4325 elif center is not None:
4326 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
4327 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
4328 graph.stroke(path.path(path._moveto(*bottom1),
4329 graph._connect(*(bottom1+bottom2)),
4330 path._moveto(*bottom),
4331 graph._connect(*(bottom+center))),
4332 *self.errorbarattrs)
4333 else:
4334 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
4335 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
4336 bottom3 = graph._addpos(*(bottom+(0, self._errorsize)))
4337 graph.stroke(path.path(path._moveto(*bottom1),
4338 graph._connect(*(bottom1+bottom2)),
4339 path._moveto(*bottom),
4340 graph._connect(*(bottom+bottom3))),
4341 *self.errorbarattrs)
4342 if top is not None and bottom is None:
4343 if center is not None:
4344 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
4345 top2 = graph._addpos(*(top+(self._errorsize, 0)))
4346 graph.stroke(path.path(path._moveto(*top1),
4347 graph._connect(*(top1+top2)),
4348 path._moveto(*top),
4349 graph._connect(*(top+center))),
4350 *self.errorbarattrs)
4351 else:
4352 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
4353 top2 = graph._addpos(*(top+(self._errorsize, 0)))
4354 top3 = graph._addpos(*(top+(0, -self._errorsize)))
4355 graph.stroke(path.path(path._moveto(*top1),
4356 graph._connect(*(top1+top2)),
4357 path._moveto(*top),
4358 graph._connect(*(top+top3))),
4359 *self.errorbarattrs)
4360 if bottomleft is not None:
4361 if topleft is not None and bottomright is None:
4362 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
4363 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
4364 graph.stroke(path.path(path._moveto(*bottomleft1),
4365 graph._connect(*(bottomleft1+bottomleft)),
4366 graph._connect(*(bottomleft+topleft)),
4367 graph._connect(*(topleft+topleft1))),
4368 *self.errorbarattrs)
4369 elif bottomright is not None and topleft is None:
4370 bottomleft1 = graph._addpos(*(bottomleft+(0, self._errorsize)))
4371 bottomright1 = graph._addpos(*(bottomright+(0, self._errorsize)))
4372 graph.stroke(path.path(path._moveto(*bottomleft1),
4373 graph._connect(*(bottomleft1+bottomleft)),
4374 graph._connect(*(bottomleft+bottomright)),
4375 graph._connect(*(bottomright+bottomright1))),
4376 *self.errorbarattrs)
4377 elif bottomright is None and topleft is None:
4378 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
4379 bottomleft2 = graph._addpos(*(bottomleft+(0, self._errorsize)))
4380 graph.stroke(path.path(path._moveto(*bottomleft1),
4381 graph._connect(*(bottomleft1+bottomleft)),
4382 graph._connect(*(bottomleft+bottomleft2))),
4383 *self.errorbarattrs)
4384 if topright is not None:
4385 if bottomright is not None and topleft is None:
4386 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
4387 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
4388 graph.stroke(path.path(path._moveto(*topright1),
4389 graph._connect(*(topright1+topright)),
4390 graph._connect(*(topright+bottomright)),
4391 graph._connect(*(bottomright+bottomright1))),
4392 *self.errorbarattrs)
4393 elif topleft is not None and bottomright is None:
4394 topright1 = graph._addpos(*(topright+(0, -self._errorsize)))
4395 topleft1 = graph._addpos(*(topleft+(0, -self._errorsize)))
4396 graph.stroke(path.path(path._moveto(*topright1),
4397 graph._connect(*(topright1+topright)),
4398 graph._connect(*(topright+topleft)),
4399 graph._connect(*(topleft+topleft1))),
4400 *self.errorbarattrs)
4401 elif topleft is None and bottomright is None:
4402 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
4403 topright2 = graph._addpos(*(topright+(0, -self._errorsize)))
4404 graph.stroke(path.path(path._moveto(*topright1),
4405 graph._connect(*(topright1+topright)),
4406 graph._connect(*(topright+topright2))),
4407 *self.errorbarattrs)
4408 if bottomright is not None and bottomleft is None and topright is None:
4409 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
4410 bottomright2 = graph._addpos(*(bottomright+(0, self._errorsize)))
4411 graph.stroke(path.path(path._moveto(*bottomright1),
4412 graph._connect(*(bottomright1+bottomright)),
4413 graph._connect(*(bottomright+bottomright2))),
4414 *self.errorbarattrs)
4415 if topleft is not None and bottomleft is None and topright is None:
4416 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
4417 topleft2 = graph._addpos(*(topleft+(0, -self._errorsize)))
4418 graph.stroke(path.path(path._moveto(*topleft1),
4419 graph._connect(*(topleft1+topleft)),
4420 graph._connect(*(topleft+topleft2))),
4421 *self.errorbarattrs)
4422 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4423 graph.stroke(path.path(path._moveto(*bottomleft),
4424 graph._connect(*(bottomleft+bottomright)),
4425 graph._connect(*(bottomright+topright)),
4426 graph._connect(*(topright+topleft)),
4427 path.closepath()),
4428 *self.errorbarattrs)
4430 def _drawsymbol(self, canvas, x, y, point=None):
4431 canvas.draw(path.path(*self.symbol(self, x, y)), *self.symbolattrs)
4433 def drawsymbol(self, canvas, x, y, point=None):
4434 self._drawsymbol(canvas, unit.topt(x), unit.topt(y), point)
4436 def key(self, c, x, y, width, height):
4437 if self._symbolattrs is not None:
4438 self._drawsymbol(c, x + 0.5 * width, y + 0.5 * height)
4439 if self._lineattrs is not None:
4440 c.stroke(path._line(x, y + 0.5 * height, x + width, y + 0.5 * height), *self.lineattrs)
4442 def drawpoints(self, graph, points):
4443 xaxismin, xaxismax = self.xaxis.getdatarange()
4444 yaxismin, yaxismax = self.yaxis.getdatarange()
4445 self.size = unit.length(_getattr(self.size_str), default_type="v")
4446 self._size = unit.topt(self.size)
4447 self.symbol = _getattr(self._symbol)
4448 self.symbolattrs = _getattrs(helper.ensuresequence(self._symbolattrs))
4449 self.errorbarattrs = _getattrs(helper.ensuresequence(self._errorbarattrs))
4450 self._errorsize = self.errorscale * self._size
4451 self.errorsize = self.errorscale * self.size
4452 self.lineattrs = _getattrs(helper.ensuresequence(self._lineattrs))
4453 if self._lineattrs is not None:
4454 clipcanvas = graph.clipcanvas()
4455 lineels = []
4456 haserror = filter(None, (self.xmini, self.ymini, self.xmaxi, self.ymaxi,
4457 self.dxi, self.dyi, self.dxmini, self.dymini, self.dxmaxi, self.dymaxi)) is not None
4458 moveto = 1
4459 for point in points:
4460 drawsymbol = 1
4461 xmin, x, xmax = self.minmidmax(point, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4462 ymin, y, ymax = self.minmidmax(point, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4463 if x is not None and x < xaxismin: drawsymbol = 0
4464 elif x is not None and x > xaxismax: drawsymbol = 0
4465 elif y is not None and y < yaxismin: drawsymbol = 0
4466 elif y is not None and y > yaxismax: drawsymbol = 0
4467 elif haserror:
4468 if xmin is not None and xmin < xaxismin: drawsymbol = 0
4469 elif xmax is not None and xmax < xaxismin: drawsymbol = 0
4470 elif xmax is not None and xmax > xaxismax: drawsymbol = 0
4471 elif xmin is not None and xmin > xaxismax: drawsymbol = 0
4472 elif ymin is not None and ymin < yaxismin: drawsymbol = 0
4473 elif ymax is not None and ymax < yaxismin: drawsymbol = 0
4474 elif ymax is not None and ymax > yaxismax: drawsymbol = 0
4475 elif ymin is not None and ymin > yaxismax: drawsymbol = 0
4476 xpos=ypos=topleft=top=topright=left=center=right=bottomleft=bottom=bottomright=None
4477 if x is not None and y is not None:
4478 try:
4479 center = xpos, ypos = graph._pos(x, y, xaxis=self.xaxis, yaxis=self.yaxis)
4480 except (ValueError, OverflowError): # XXX: exceptions???
4481 pass
4482 if haserror:
4483 if y is not None:
4484 if xmin is not None: left = graph._pos(xmin, y, xaxis=self.xaxis, yaxis=self.yaxis)
4485 if xmax is not None: right = graph._pos(xmax, y, xaxis=self.xaxis, yaxis=self.yaxis)
4486 if x is not None:
4487 if ymax is not None: top = graph._pos(x, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4488 if ymin is not None: bottom = graph._pos(x, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4489 if x is None or y is None:
4490 if ymax is not None:
4491 if xmin is not None: topleft = graph._pos(xmin, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4492 if xmax is not None: topright = graph._pos(xmax, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4493 if ymin is not None:
4494 if xmin is not None: bottomleft = graph._pos(xmin, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4495 if xmax is not None: bottomright = graph._pos(xmax, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4496 if drawsymbol:
4497 if self._errorbarattrs is not None and haserror:
4498 self._drawerrorbar(graph, topleft, top, topright,
4499 left, center, right,
4500 bottomleft, bottom, bottomright, point)
4501 if self._symbolattrs is not None and xpos is not None and ypos is not None:
4502 self._drawsymbol(graph, xpos, ypos, point)
4503 if xpos is not None and ypos is not None:
4504 if moveto:
4505 lineels.append(path._moveto(xpos, ypos))
4506 moveto = 0
4507 else:
4508 lineels.append(path._lineto(xpos, ypos))
4509 else:
4510 moveto = 1
4511 self.path = path.path(*lineels)
4512 if self._lineattrs is not None:
4513 clipcanvas.stroke(self.path, *self.lineattrs)
4516 class changesymbol(changesequence): pass
4519 class _changesymbolcross(changesymbol):
4520 defaultsequence = (symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond)
4523 class _changesymbolplus(changesymbol):
4524 defaultsequence = (symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross)
4527 class _changesymbolsquare(changesymbol):
4528 defaultsequence = (symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus)
4531 class _changesymboltriangle(changesymbol):
4532 defaultsequence = (symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square)
4535 class _changesymbolcircle(changesymbol):
4536 defaultsequence = (symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle)
4539 class _changesymboldiamond(changesymbol):
4540 defaultsequence = (symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle)
4543 class _changesymbolsquaretwice(changesymbol):
4544 defaultsequence = (symbol.square, symbol.square, symbol.triangle, symbol.triangle,
4545 symbol.circle, symbol.circle, symbol.diamond, symbol.diamond)
4548 class _changesymboltriangletwice(changesymbol):
4549 defaultsequence = (symbol.triangle, symbol.triangle, symbol.circle, symbol.circle,
4550 symbol.diamond, symbol.diamond, symbol.square, symbol.square)
4553 class _changesymbolcircletwice(changesymbol):
4554 defaultsequence = (symbol.circle, symbol.circle, symbol.diamond, symbol.diamond,
4555 symbol.square, symbol.square, symbol.triangle, symbol.triangle)
4558 class _changesymboldiamondtwice(changesymbol):
4559 defaultsequence = (symbol.diamond, symbol.diamond, symbol.square, symbol.square,
4560 symbol.triangle, symbol.triangle, symbol.circle, symbol.circle)
4563 changesymbol.cross = _changesymbolcross
4564 changesymbol.plus = _changesymbolplus
4565 changesymbol.square = _changesymbolsquare
4566 changesymbol.triangle = _changesymboltriangle
4567 changesymbol.circle = _changesymbolcircle
4568 changesymbol.diamond = _changesymboldiamond
4569 changesymbol.squaretwice = _changesymbolsquaretwice
4570 changesymbol.triangletwice = _changesymboltriangletwice
4571 changesymbol.circletwice = _changesymbolcircletwice
4572 changesymbol.diamondtwice = _changesymboldiamondtwice
4575 class line(symbol):
4577 def __init__(self, lineattrs=helper.nodefault):
4578 if lineattrs is helper.nodefault:
4579 lineattrs = (changelinestyle(), canvas.linejoin.round)
4580 symbol.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
4583 class rect(symbol):
4585 def __init__(self, palette=color.palette.Gray):
4586 self.palette = palette
4587 self.colorindex = None
4588 symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
4590 def iterate(self):
4591 raise RuntimeError("style is not iterateable")
4593 def othercolumnkey(self, key, index):
4594 if key == "color":
4595 self.colorindex = index
4596 else:
4597 symbol.othercolumnkey(self, key, index)
4599 def _drawerrorbar(self, graph, topleft, top, topright,
4600 left, center, right,
4601 bottomleft, bottom, bottomright, point=None):
4602 color = point[self.colorindex]
4603 if color is not None:
4604 if color != self.lastcolor:
4605 self.rectclipcanvas.set(self.palette.getcolor(color))
4606 if bottom is not None and left is not None:
4607 bottomleft = left[0], bottom[1]
4608 if bottom is not None and right is not None:
4609 bottomright = right[0], bottom[1]
4610 if top is not None and right is not None:
4611 topright = right[0], top[1]
4612 if top is not None and left is not None:
4613 topleft = left[0], top[1]
4614 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4615 self.rectclipcanvas.fill(path.path(path._moveto(*bottomleft),
4616 graph._connect(*(bottomleft+bottomright)),
4617 graph._connect(*(bottomright+topright)),
4618 graph._connect(*(topright+topleft)),
4619 path.closepath()))
4621 def drawpoints(self, graph, points):
4622 if self.colorindex is None:
4623 raise RuntimeError("column 'color' not set")
4624 self.lastcolor = None
4625 self.rectclipcanvas = graph.clipcanvas()
4626 symbol.drawpoints(self, graph, points)
4628 def key(self, c, x, y, width, height):
4629 raise RuntimeError("style doesn't yet provide a key")
4632 class text(symbol):
4634 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=textmodule.halign.center, **args):
4635 self.textindex = None
4636 self.textdx_str = textdx
4637 self.textdy_str = textdy
4638 self._textattrs = textattrs
4639 symbol.__init__(self, **args)
4641 def iteratedict(self):
4642 result = symbol.iteratedict()
4643 result["textattrs"] = _iterateattr(self._textattrs)
4644 return result
4646 def iterate(self):
4647 return textsymbol(**self.iteratedict())
4649 def othercolumnkey(self, key, index):
4650 if key == "text":
4651 self.textindex = index
4652 else:
4653 symbol.othercolumnkey(self, key, index)
4655 def _drawsymbol(self, graph, x, y, point=None):
4656 symbol._drawsymbol(self, graph, x, y, point)
4657 if None not in (x, y, point[self.textindex]) and self._textattrs is not None:
4658 graph._text(x + self._textdx, y + self._textdy, str(point[self.textindex]), *helper.ensuresequence(self.textattrs))
4660 def drawpoints(self, graph, points):
4661 self.textdx = unit.length(_getattr(self.textdx_str), default_type="v")
4662 self.textdy = unit.length(_getattr(self.textdy_str), default_type="v")
4663 self._textdx = unit.topt(self.textdx)
4664 self._textdy = unit.topt(self.textdy)
4665 if self._textattrs is not None:
4666 self.textattrs = _getattr(self._textattrs)
4667 if self.textindex is None:
4668 raise RuntimeError("column 'text' not set")
4669 symbol.drawpoints(self, graph, points)
4671 def key(self, c, x, y, width, height):
4672 raise RuntimeError("style doesn't yet provide a key")
4675 class arrow(symbol):
4677 def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
4678 self.linelength_str = linelength
4679 self.arrowsize_str = arrowsize
4680 self.arrowattrs = arrowattrs
4681 self.arrowdict = arrowdict
4682 self.epsilon = epsilon
4683 self.sizeindex = self.angleindex = None
4684 symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
4686 def iterate(self):
4687 raise RuntimeError("style is not iterateable")
4689 def othercolumnkey(self, key, index):
4690 if key == "size":
4691 self.sizeindex = index
4692 elif key == "angle":
4693 self.angleindex = index
4694 else:
4695 symbol.othercolumnkey(self, key, index)
4697 def _drawsymbol(self, graph, x, y, point=None):
4698 if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
4699 if point[self.sizeindex] > self.epsilon:
4700 dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
4701 x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
4702 y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
4703 x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
4704 y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
4705 graph.stroke(path.line(x1, y1, x2, y2),
4706 canvas.earrow(self.arrowsize*point[self.sizeindex],
4707 **self.arrowdict),
4708 *helper.ensuresequence(self.arrowattrs))
4710 def drawpoints(self, graph, points):
4711 self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
4712 self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
4713 self._arrowsize = unit.topt(self.arrowsize)
4714 self._linelength = unit.topt(self.linelength)
4715 if self.sizeindex is None:
4716 raise RuntimeError("column 'size' not set")
4717 if self.angleindex is None:
4718 raise RuntimeError("column 'angle' not set")
4719 symbol.drawpoints(self, graph, points)
4721 def key(self, c, x, y, width, height):
4722 raise RuntimeError("style doesn't yet provide a key")
4725 class _bariterator(changeattr):
4727 def attr(self, index):
4728 return index, self.counter
4731 class bar:
4733 def __init__(self, fromzero=1, stacked=0, skipmissing=1, xbar=0,
4734 barattrs=helper.nodefault, _usebariterator=helper.nodefault, _previousbar=None):
4735 self.fromzero = fromzero
4736 self.stacked = stacked
4737 self.skipmissing = skipmissing
4738 self.xbar = xbar
4739 if barattrs is helper.nodefault:
4740 self._barattrs = (canvas.stroked(color.gray.black), changecolor.Rainbow())
4741 else:
4742 self._barattrs = barattrs
4743 if _usebariterator is helper.nodefault:
4744 self.bariterator = _bariterator()
4745 else:
4746 self.bariterator = _usebariterator
4747 self.previousbar = _previousbar
4749 def iteratedict(self):
4750 result = {}
4751 result["barattrs"] = _iterateattrs(self._barattrs)
4752 return result
4754 def iterate(self):
4755 return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
4756 _usebariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
4758 def setcolumns(self, graph, columns):
4759 def checkpattern(key, index, pattern, iskey, isindex):
4760 if key is not None:
4761 match = pattern.match(key)
4762 if match:
4763 if isindex is not None: raise ValueError("multiple key specification")
4764 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4765 key = None
4766 iskey = match.groups()[0]
4767 isindex = index
4768 return key, iskey, isindex
4770 xkey = ykey = None
4771 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4772 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4773 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4774 xi = yi = None
4775 for key, index in columns.items():
4776 key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
4777 key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
4778 if key is not None:
4779 self.othercolumnkey(key, index)
4780 if None in (xkey, ykey): raise ValueError("incomplete axis specification")
4781 if self.xbar:
4782 self.nkey, self.ni = ykey, yi
4783 self.vkey, self.vi = xkey, xi
4784 else:
4785 self.nkey, self.ni = xkey, xi
4786 self.vkey, self.vi = ykey, yi
4787 self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
4789 def getranges(self, points):
4790 index, count = _getattr(self.bariterator)
4791 if count != 1 and self.stacked != 1:
4792 if self.stacked > 1:
4793 index = divmod(index, self.stacked)[0]
4795 vmin = vmax = None
4796 for point in points:
4797 if not self.skipmissing:
4798 if count != 1 and self.stacked != 1:
4799 self.naxis.setname(point[self.ni], index)
4800 else:
4801 self.naxis.setname(point[self.ni])
4802 try:
4803 v = point[self.vi] + 0.0
4804 if vmin is None or v < vmin: vmin = v
4805 if vmax is None or v > vmax: vmax = v
4806 except (TypeError, ValueError):
4807 pass
4808 else:
4809 if self.skipmissing:
4810 if count != 1 and self.stacked != 1:
4811 self.naxis.setname(point[self.ni], index)
4812 else:
4813 self.naxis.setname(point[self.ni])
4814 if self.fromzero:
4815 if vmin > 0: vmin = 0
4816 if vmax < 0: vmax = 0
4817 return {self.vkey: (vmin, vmax)}
4819 def drawpoints(self, graph, points):
4820 index, count = _getattr(self.bariterator)
4821 dostacked = (self.stacked != 0 and
4822 (self.stacked == 1 or divmod(index, self.stacked)[1]) and
4823 (self.stacked != 1 or index))
4824 if self.stacked > 1:
4825 index = divmod(index, self.stacked)[0]
4826 vmin, vmax = self.vaxis.getdatarange()
4827 self.barattrs = _getattrs(helper.ensuresequence(self._barattrs))
4828 if self.stacked:
4829 self.stackedvalue = {}
4830 for point in points:
4831 try:
4832 n = point[self.ni]
4833 v = point[self.vi]
4834 if self.stacked:
4835 self.stackedvalue[n] = v
4836 if count != 1 and self.stacked != 1:
4837 minid = (n, index, 0)
4838 maxid = (n, index, 1)
4839 else:
4840 minid = (n, 0)
4841 maxid = (n, 1)
4842 if self.xbar:
4843 x1pos, y1pos = graph._pos(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
4844 x2pos, y2pos = graph._pos(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4845 else:
4846 x1pos, y1pos = graph._pos(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
4847 x2pos, y2pos = graph._pos(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
4848 if dostacked:
4849 if self.xbar:
4850 x3pos, y3pos = graph._pos(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
4851 x4pos, y4pos = graph._pos(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
4852 else:
4853 x3pos, y3pos = graph._pos(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4854 x4pos, y4pos = graph._pos(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4855 else:
4856 if self.fromzero:
4857 if self.xbar:
4858 x3pos, y3pos = graph._pos(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4859 x4pos, y4pos = graph._pos(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
4860 else:
4861 x3pos, y3pos = graph._pos(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4862 x4pos, y4pos = graph._pos(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4863 else:
4864 x3pos, y3pos = graph._tickpoint(maxid, axis=self.naxis)
4865 x4pos, y4pos = graph._tickpoint(minid, axis=self.naxis)
4866 if self.barattrs is not None:
4867 graph.fill(path.path(path._moveto(x1pos, y1pos),
4868 graph._connect(x1pos, y1pos, x2pos, y2pos),
4869 graph._connect(x2pos, y2pos, x3pos, y3pos),
4870 graph._connect(x3pos, y3pos, x4pos, y4pos),
4871 graph._connect(x4pos, y4pos, x1pos, y1pos), # no closepath (might not be straight)
4872 path.closepath()), *self.barattrs)
4873 except (TypeError, ValueError): pass
4875 def key(self, c, x, y, width, height):
4876 c.fill(path._rect(x, y, width, height), *self.barattrs)
4879 #class surface:
4881 # def setcolumns(self, graph, columns):
4882 # self.columns = columns
4884 # def getranges(self, points):
4885 # return {"x": (0, 10), "y": (0, 10), "z": (0, 1)}
4887 # def drawpoints(self, graph, points):
4888 # pass
4892 ################################################################################
4893 # data
4894 ################################################################################
4897 class data:
4899 defaultstyle = symbol
4901 def __init__(self, file, title=helper.nodefault, context={}, **columns):
4902 self.title = title
4903 if helper.isstring(file):
4904 self.data = datamodule.datafile(file)
4905 else:
4906 self.data = file
4907 if title is helper.nodefault:
4908 self.title = "(unknown)"
4909 else:
4910 self.title = title
4911 self.columns = {}
4912 for key, column in columns.items():
4913 try:
4914 self.columns[key] = self.data.getcolumnno(column)
4915 except datamodule.ColumnError:
4916 self.columns[key] = len(self.data.titles)
4917 self.data.addcolumn(column, context=context)
4919 def setstyle(self, graph, style):
4920 self.style = style
4921 self.style.setcolumns(graph, self.columns)
4923 def getranges(self):
4924 return self.style.getranges(self.data.data)
4926 def setranges(self, ranges):
4927 pass
4929 def draw(self, graph):
4930 self.style.drawpoints(graph, self.data.data)
4933 class function:
4935 defaultstyle = line
4937 def __init__(self, expression, title=helper.nodefault, min=None, max=None, points=100, parser=mathtree.parser(), context={}):
4938 if title is helper.nodefault:
4939 self.title = expression
4940 else:
4941 self.title = title
4942 self.min = min
4943 self.max = max
4944 self.points = points
4945 self.context = context
4946 self.result, expression = expression.split("=")
4947 self.mathtree = parser.parse(expression)
4948 self.variable = None
4949 self.evalranges = 0
4951 def setstyle(self, graph, style):
4952 for variable in self.mathtree.VarList():
4953 if variable in graph.axes.keys():
4954 if self.variable is None:
4955 self.variable = variable
4956 else:
4957 raise ValueError("multiple variables found")
4958 if self.variable is None:
4959 raise ValueError("no variable found")
4960 self.xaxis = graph.axes[self.variable]
4961 self.style = style
4962 self.style.setcolumns(graph, {self.variable: 0, self.result: 1})
4964 def getranges(self):
4965 if self.evalranges:
4966 return self.style.getranges(self.data)
4967 if None not in (self.min, self.max):
4968 return {self.variable: (self.min, self.max)}
4970 def setranges(self, ranges):
4971 if ranges.has_key(self.variable):
4972 min, max = ranges[self.variable]
4973 if self.min is not None: min = self.min
4974 if self.max is not None: max = self.max
4975 vmin = self.xaxis.convert(min)
4976 vmax = self.xaxis.convert(max)
4977 self.data = []
4978 for i in range(self.points):
4979 self.context[self.variable] = x = self.xaxis.invert(vmin + (vmax-vmin)*i / (self.points-1.0))
4980 try:
4981 y = self.mathtree.Calc(**self.context)
4982 except (ArithmeticError, ValueError):
4983 y = None
4984 self.data.append((x, y))
4985 self.evalranges = 1
4987 def draw(self, graph):
4988 self.style.drawpoints(graph, self.data)
4991 class paramfunction:
4993 defaultstyle = line
4995 def __init__(self, varname, min, max, expression, title=helper.nodefault, points=100, parser=mathtree.parser(), context={}):
4996 if title is helper.nodefault:
4997 self.title = expression
4998 else:
4999 self.title = title
5000 self.varname = varname
5001 self.min = min
5002 self.max = max
5003 self.points = points
5004 self.expression = {}
5005 self.mathtrees = {}
5006 varlist, expressionlist = expression.split("=")
5007 parsestr = mathtree.ParseStr(expressionlist)
5008 for key in varlist.split(","):
5009 key = key.strip()
5010 if self.mathtrees.has_key(key):
5011 raise ValueError("multiple assignment in tuple")
5012 try:
5013 self.mathtrees[key] = parser.ParseMathTree(parsestr)
5014 break
5015 except mathtree.CommaFoundMathTreeParseError, e:
5016 self.mathtrees[key] = e.MathTree
5017 else:
5018 raise ValueError("unpack tuple of wrong size")
5019 if len(varlist.split(",")) != len(self.mathtrees.keys()):
5020 raise ValueError("unpack tuple of wrong size")
5021 self.data = []
5022 for i in range(self.points):
5023 context[self.varname] = self.min + (self.max-self.min)*i / (self.points-1.0)
5024 line = []
5025 for key, tree in self.mathtrees.items():
5026 line.append(tree.Calc(**context))
5027 self.data.append(line)
5029 def setstyle(self, graph, style):
5030 self.style = style
5031 columns = {}
5032 for key, index in zip(self.mathtrees.keys(), xrange(sys.maxint)):
5033 columns[key] = index
5034 self.style.setcolumns(graph, columns)
5036 def getranges(self):
5037 return self.style.getranges(self.data)
5039 def setranges(self, ranges):
5040 pass
5042 def draw(self, graph):
5043 self.style.drawpoints(graph, self.data)