pdftex; texter (graph is currently broken); data docstrings --- minor other stuff...
[PyX/mjg.git] / pyx / graph.py
blob6c5eef62ceff87200345bd7af8fc0fe967742c15
1 #!/usr/bin/env python
4 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 import types, re, math, string, sys
25 import bbox, box, canvas, path, unit, mathtree, trafo, color, helper
26 import text as textmodule
27 import data as datamodule
30 goldenmean = 0.5 * (math.sqrt(5) + 1)
33 ################################################################################
34 # maps
35 ################################################################################
37 class _Imap:
38 """interface definition of a map
39 maps convert a value into another value by bijective transformation f"""
41 def convert(self, x):
42 "returns f(x)"
44 def invert(self, y):
45 "returns f^-1(y) where f^-1 is the inverse transformation (x=f^-1(f(x)) for all x)"
47 def setbasepoint(self, basepoints):
48 """set basepoints for the convertions
49 basepoints are tuples (x, y) with y == f(x) and x == f^-1(y)
50 the number of basepoints needed might depend on the transformation
51 usually two pairs are needed like for linear maps, logarithmic maps, etc."""
54 class _linmap:
55 "linear mapping"
56 __implements__ = _Imap
58 def setbasepoints(self, basepoints):
59 "method is part of the implementation of _Imap"
60 self.dydx = (basepoints[1][1] - basepoints[0][1]) / float(basepoints[1][0] - basepoints[0][0])
61 self.dxdy = (basepoints[1][0] - basepoints[0][0]) / float(basepoints[1][1] - basepoints[0][1])
62 self.x1 = basepoints[0][0]
63 self.y1 = basepoints[0][1]
64 return self
66 def convert(self, value):
67 "method is part of the implementation of _Imap"
68 return self.y1 + self.dydx * (value - self.x1)
70 def invert(self, value):
71 "method is part of the implementation of _Imap"
72 return self.x1 + self.dxdy * (value - self.y1)
75 class _logmap:
76 "logarithmic mapping"
77 __implements__ = _Imap
79 def setbasepoints(self, basepoints):
80 self.dydx = ((basepoints[1][1] - basepoints[0][1]) /
81 float(math.log(basepoints[1][0]) - math.log(basepoints[0][0])))
82 self.dxdy = ((math.log(basepoints[1][0]) - math.log(basepoints[0][0])) /
83 float(basepoints[1][1] - basepoints[0][1]))
84 self.x1 = math.log(basepoints[0][0])
85 self.y1 = basepoints[0][1]
86 return self
88 def convert(self, value):
89 return self.y1 + self.dydx * (math.log(value) - self.x1)
91 def invert(self, value):
92 return math.exp(self.x1 + self.dxdy * (value - self.y1))
96 ################################################################################
97 # partition schemes
98 # please note the nomenclature:
99 # - a partition is a ordered sequence of tick instances
100 # - a partition scheme is a class creating a single or several partitions
101 ################################################################################
104 class frac:
105 """fraction class for rational arithmetics
106 the axis partitioning uses rational arithmetics (with infinite accuracy)
107 basically it contains self.enum and self.denom"""
109 def __init__(self, enum, denom, power=None):
110 "for power!=None: frac=(enum/denom)**power"
111 if not helper.isinteger(enum) or not helper.isinteger(denom): raise TypeError("integer type expected")
112 if not denom: raise ZeroDivisionError("zero denominator")
113 if power != None:
114 if not helper.isinteger(power): raise TypeError("integer type expected")
115 if power >= 0:
116 self.enum = long(enum) ** power
117 self.denom = long(denom) ** power
118 else:
119 self.enum = long(denom) ** (-power)
120 self.denom = long(enum) ** (-power)
121 else:
122 self.enum = enum
123 self.denom = denom
125 def __cmp__(self, other):
126 if other is None:
127 return 1
128 return cmp(self.enum * other.denom, other.enum * self.denom)
130 def __mul__(self, other):
131 return frac(self.enum * other.enum, self.denom * other.denom)
133 def __float__(self):
134 "caution: avoid final precision of floats"
135 return float(self.enum) / self.denom
137 def __str__(self):
138 return "%i/%i" % (self.enum, self.denom)
141 def _ensurefrac(arg):
142 """helper function to convert arg into a frac
143 strings like 0.123, 1/-2, 1.23/34.21 are converted to a frac"""
144 # TODO: exponentials are not yet supported, e.g. 1e-10, etc.
145 # XXX: we don't need to force long on newer python versions
147 def createfrac(str):
148 "converts a string 0.123 into a frac"
149 commaparts = str.split(".")
150 for part in commaparts:
151 if not part.isdigit(): raise ValueError("non-digits found in '%s'" % part)
152 if len(commaparts) == 1:
153 return frac(long(commaparts[0]), 1)
154 elif len(commaparts) == 2:
155 result = frac(1, 10l, power=len(commaparts[1]))
156 result.enum = long(commaparts[0])*result.denom + long(commaparts[1])
157 return result
158 else: raise ValueError("multiple '.' found in '%s'" % str)
160 if helper.isstring(arg):
161 fraction = arg.split("/")
162 if len(fraction) > 2: raise ValueError("multiple '/' found in '%s'" % arg)
163 value = createfrac(fraction[0])
164 if len(fraction) == 2:
165 value2 = createfrac(fraction[1])
166 value = frac(value.enum * value2.denom, value.denom * value2.enum)
167 return value
168 if not isinstance(arg, frac): raise ValueError("can't convert argument to frac")
169 return arg
172 class tick(frac):
173 """tick class
174 a tick is a frac enhanced by
175 - self.ticklevel (0 = tick, 1 = subtick, etc.)
176 - self.labellevel (0 = label, 1 = sublabel, etc.)
177 - self.label (a string) and self.labelttrs (a list, defaults to [])
178 When ticklevel or labellevel is None, no tick or label is present at that value.
179 When label is None, it should be automatically created (and stored), once the
180 an axis painter needs it. Classes, which implement _Itexter do precisely that."""
182 def __init__(self, enum, denom, ticklevel=None, labellevel=None, label=None, labelattrs=[]):
183 frac.__init__(self, enum, denom)
184 self.ticklevel = ticklevel
185 self.labellevel = labellevel
186 self.label = label
187 self.labelattrs = labelattrs
189 def merge(self, other):
190 """merges two ticks together:
191 - the lower ticklevel/labellevel wins
192 - when present, self.label is taken over; otherwise the others label is taken
193 - the ticks should be at the same position (otherwise it doesn't make sense)
194 -> this is NOT checked
196 if self.ticklevel is None or (other.ticklevel is not None and other.ticklevel < self.ticklevel):
197 self.ticklevel = other.ticklevel
198 if self.labellevel is None or (other.labellevel is not None and other.labellevel < self.labellevel):
199 self.labellevel = other.labellevel
200 if self.label is None:
201 self.label = other.label
203 def __repr__(self):
204 return "tick(%r, %r, %s, %s, %s)" % (self.enum, self.denom, self.ticklevel, self.labellevel, self.label)
207 def _mergeticklists(list1, list2):
208 """helper function to merge tick lists
209 return a merged list of ticks out of list1 and list2
210 caution: original lists have to be ordered
211 (the returned list is also ordered)
212 caution: original lists are modified and they share references to
213 the result list!"""
214 # TODO: improve this using bisect
215 i = 0
216 j = 0
217 try:
218 while 1: # we keep on going until we reach an index error
219 while list2[j] < list1[i]: # insert tick
220 list1.insert(i, list2[j])
221 i += 1
222 j += 1
223 if list2[j] == list1[i]: # merge tick
224 list1[i].merge(list2[j])
225 j += 1
226 i += 1
227 except IndexError:
228 if j < len(list2):
229 list1 += list2[j:]
230 return list1
233 def _mergelabels(ticks, labels):
234 """helper function to merge labels into ticks
235 - when labels is not None, the label of all ticks with
236 labellevel
237 different from None are set
238 - labels need to be a sequence of sequences of strings,
239 where the first sequence contain the strings to be
240 used as labels for the ticks with labellevel 0,
241 the second sequence for labellevel 1, etc.
242 - when the maximum labellevel is 0, just a sequence of
243 strings might be provided as the labels argument
244 - IndexError is raised, when a sequence length doesn't match"""
245 if helper.issequenceofsequences(labels):
246 for label, level in zip(labels, xrange(sys.maxint)):
247 usetext = helper.ensuresequence(label)
248 i = 0
249 for tick in ticks:
250 if tick.labellevel == level:
251 tick.label = usetext[i]
252 i += 1
253 if i != len(usetext):
254 raise IndexError("wrong sequence length of labels at level %i" % level)
255 elif labels is not None:
256 usetext = helper.ensuresequence(labels)
257 i = 0
258 for tick in ticks:
259 if tick.labellevel == 0:
260 tick.label = usetext[i]
261 i += 1
262 if i != len(usetext):
263 raise IndexError("wrong sequence length of labels")
266 class _Ipart:
267 """interface definition of a partition scheme
268 partition schemes are used to create a list of ticks"""
270 def defaultpart(self, min, max, extendmin, extendmax):
271 """create a partition
272 - returns an ordered list of ticks for the interval min to max
273 - the interval is given in float numbers, thus an appropriate
274 conversion to rational numbers has to be performed
275 - extendmin and extendmax are booleans (integers)
276 - when extendmin or extendmax is set, the ticks might
277 extend the min-max range towards lower and higher
278 ranges, respectively"""
280 def lesspart(self):
281 """create another partition which contains less ticks
282 - this method is called several times after a call of defaultpart
283 - returns an ordered list of ticks with less ticks compared to
284 the partition returned by defaultpart and by previous calls
285 of lesspart
286 - the creation of a partition with strictly *less* ticks
287 is not to be taken serious
288 - the method might return None, when no other appropriate
289 partition can be created"""
292 def morepart(self):
293 """create another partition which contains more ticks
294 see lesspart, but increase the number of ticks"""
297 class manualpart:
298 """manual partition scheme
299 ticks and labels at positions explicitly provided to the constructor"""
301 __implements__ = _Ipart
303 def __init__(self, ticks=None, labels=None, texts=None, mix=()):
304 """configuration of the partition scheme
305 - ticks and labels should be a sequence of sequences, where
306 the first sequence contains the values to be used for
307 ticks with ticklevel/labellevel 0, the second sequence for
308 ticklevel/labellevel 1, etc.
309 - tick and label values must be frac instances or
310 strings convertable to fracs by the _ensurefrac function
311 - when the maximum ticklevel/labellevel is 0, just a sequence
312 might be provided in ticks and labels
313 - when labels is None and ticks is not None, the tick entries
314 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
315 - labels are applied to the resulting partition via the
316 mergelabels function (additional information available there)
317 - mix specifies another partition to be merged into the
318 created partition"""
319 if ticks is None and labels is not None:
320 self.ticks = helper.ensuresequence(helper.getsequenceno(labels, 0))
321 else:
322 self.ticks = ticks
323 if labels is None and ticks is not None:
324 self.labels = helper.ensuresequence(helper.getsequenceno(ticks, 0))
325 else:
326 self.labels = labels
327 self.texts = texts
328 self.mix = mix
330 def checkfraclist(self, *fracs):
331 """orders a list of fracs, equal entries are not allowed"""
332 if not len(fracs): return ()
333 sorted = list(fracs)
334 sorted.sort()
335 last = sorted[0]
336 for item in sorted[1:]:
337 if last == item:
338 raise ValueError("duplicate entry found")
339 last = item
340 return sorted
342 def part(self):
343 "create the partition as described in the constructor"
344 ticks = list(self.mix)
345 if helper.issequenceofsequences(self.ticks):
346 for fracs, level in zip(self.ticks, xrange(sys.maxint)):
347 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, ticklevel = level)
348 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(fracs)))])
349 else:
350 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, ticklevel = 0)
351 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(self.ticks)))])
353 if helper.issequenceofsequences(self.labels):
354 for fracs, level in zip(self.labels, xrange(sys.maxint)):
355 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, labellevel = level)
356 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(fracs)))])
357 else:
358 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, labellevel = 0)
359 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(self.labels)))])
361 _mergelabels(ticks, self.labels)
363 return ticks
365 def defaultpart(self, min, max, extendmin, extendmax):
366 """method is part of the implementation of _Ipart
367 XXX: we do not take care of the parameters -> correct?"""
368 return self.part()
370 def lesspart(self):
371 "method is part of the implementation of _Ipart"
372 return None
374 def morepart(self):
375 "method is part of the implementation of _Ipart"
376 return None
379 class linpart:
380 """linear partition scheme
381 ticks and label distances are explicitly provided to the constructor"""
383 __implements__ = _Ipart
385 def __init__(self, ticks=None, labels=None, texts=None, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
386 """configuration of the partition scheme
387 - ticks and labels should be a sequence, where the first value
388 is the distance between ticks with ticklevel/labellevel 0,
389 the second sequence for ticklevel/labellevel 1, etc.
390 - tick and label values must be frac instances or
391 strings convertable to fracs by the _ensurefrac function
392 - when the maximum ticklevel/labellevel is 0, just a single value
393 might be provided in ticks and labels
394 - when labels is None and ticks is not None, the tick entries
395 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
396 - texts are applied to the resulting partition via the
397 mergetexts function (additional information available there)
398 - extendtick allows for the extension of the range given to the
399 defaultpart method to include the next tick with the specified
400 level (None turns off this feature); note, that this feature is
401 also disabled, when an axis prohibits its range extension by
402 the extendmin/extendmax variables given to the defaultpart method
403 - extendlabel is analogous to extendtick, but for labels
404 - epsilon allows for exceeding the axis range by this relative
405 value (relative to the axis range given to the defaultpart method)
406 without creating another tick specified by extendtick/extendlabel
407 - mix specifies another partition to be merged into the
408 created partition"""
409 if ticks is None and labels is not None:
410 self.ticks = (_ensurefrac(helper.ensuresequence(labels)[0]),)
411 else:
412 self.ticks = map(_ensurefrac, helper.ensuresequence(ticks))
413 if labels is None and ticks is not None:
414 self.labels = (_ensurefrac(helper.ensuresequence(ticks)[0]),)
415 else:
416 self.labels = map(_ensurefrac, helper.ensuresequence(labels))
417 self.labels = labels
418 self.extendtick = extendtick
419 self.extendlabel = extendlabel
420 self.epsilon = epsilon
421 self.mix = mix
423 def extendminmax(self, min, max, frac, extendmin, extendmax):
424 """return new min, max tuple extending the range min, max
425 frac is the tick distance to be used
426 extendmin and extendmax are booleans to allow for the extension"""
427 if extendmin:
428 min = float(frac) * math.floor(min / float(frac) + self.epsilon)
429 if extendmax:
430 max = float(frac) * math.ceil(max / float(frac) - self.epsilon)
431 return min, max
433 def getticks(self, min, max, frac, ticklevel=None, labellevel=None):
434 """return a list of equal spaced ticks
435 - the tick distance is frac, the ticklevel is set to ticklevel and
436 the labellevel is set to labellevel
437 - min, max is the range where ticks should be placed"""
438 imin = int(math.ceil(min / float(frac) - 0.5 * self.epsilon))
439 imax = int(math.floor(max / float(frac) + 0.5 * self.epsilon))
440 ticks = []
441 for i in range(imin, imax + 1):
442 ticks.append(tick(long(i) * frac.enum, frac.denom, ticklevel=ticklevel, labellevel=labellevel))
443 return ticks
445 def defaultpart(self, min, max, extendmin, extendmax):
446 "method is part of the implementation of _Ipart"
447 if self.extendtick is not None and len(self.ticks) > self.extendtick:
448 min, max = self.extendminmax(min, max, self.ticks[self.extendtick], extendmin, extendmax)
449 if self.extendlabel is not None and len(self.labels) > self.extendlabel:
450 min, max = self.extendminmax(min, max, self.labels[self.extendlabel], extendmin, extendmax)
452 ticks = list(self.mix)
453 for i in range(len(self.ticks)):
454 ticks = _mergeticklists(ticks, self.getticks(min, max, self.ticks[i], ticklevel = i))
455 for i in range(len(self.labels)):
456 ticks = _mergeticklists(ticks, self.getticks(min, max, self.labels[i], labellevel = i))
458 _mergelabels(ticks, self.labels)
460 return ticks
462 def lesspart(self):
463 "method is part of the implementation of _Ipart"
464 return None
466 def morepart(self):
467 "method is part of the implementation of _Ipart"
468 return None
471 class autolinpart:
472 """automatic linear partition scheme
473 - possible tick distances are explicitly provided to the constructor
474 - tick distances are adjusted to the axis range by multiplication or division by 10"""
476 __implements__ = _Ipart
478 defaultlist = ((frac(1, 1), frac(1, 2)),
479 (frac(2, 1), frac(1, 1)),
480 (frac(5, 2), frac(5, 4)),
481 (frac(5, 1), frac(5, 2)))
483 def __init__(self, list=defaultlist, extendtick=0, epsilon=1e-10, mix=()):
484 """configuration of the partition scheme
485 - list should be a sequence of fracs
486 - ticks should be a sequence, where the first value
487 is the distance between ticks with ticklevel 0,
488 the second for ticklevel 1, etc.
489 - tick values must be frac instances or
490 strings convertable to fracs by the _ensurefrac function
491 - labellevel is set to None except for those ticks in the partitions,
492 where ticklevel is zero. There labellevel is also set to zero.
493 - extendtick allows for the extension of the range given to the
494 defaultpart method to include the next tick with the specified
495 level (None turns off this feature); note, that this feature is
496 also disabled, when an axis prohibits its range extension by
497 the extendmin/extendmax variables given to the defaultpart method
498 - epsilon allows for exceeding the axis range by this relative
499 value (relative to the axis range given to the defaultpart method)
500 without creating another tick specified by extendtick
501 - mix specifies another partition to be merged into the
502 created partition"""
503 self.list = list
504 self.extendtick = extendtick
505 self.epsilon = epsilon
506 self.mix = mix
508 def defaultpart(self, min, max, extendmin, extendmax):
509 "method is part of the implementation of _Ipart"
510 base = frac(10L, 1, int(math.log(max - min) / math.log(10)))
511 ticks = self.list[0]
512 useticks = [tick * base for tick in ticks]
513 self.lesstickindex = self.moretickindex = 0
514 self.lessbase = frac(base.enum, base.denom)
515 self.morebase = frac(base.enum, base.denom)
516 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
517 part = linpart(ticks=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
518 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
520 def lesspart(self):
521 "method is part of the implementation of _Ipart"
522 if self.lesstickindex < len(self.list) - 1:
523 self.lesstickindex += 1
524 else:
525 self.lesstickindex = 0
526 self.lessbase.enum *= 10
527 ticks = self.list[self.lesstickindex]
528 useticks = [tick * self.lessbase for tick in ticks]
529 part = linpart(ticks=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
530 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
532 def morepart(self):
533 "method is part of the implementation of _Ipart"
534 if self.moretickindex:
535 self.moretickindex -= 1
536 else:
537 self.moretickindex = len(self.list) - 1
538 self.morebase.denom *= 10
539 ticks = self.list[self.moretickindex]
540 useticks = [tick * self.morebase for tick in ticks]
541 part = linpart(ticks=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
542 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
545 class shiftfracs:
546 """storage class for the definition of logarithmic axes partitions
547 instances of this class define tick positions suitable for
548 logarithmic axes by the following instance variables:
549 - shift: integer, which defines multiplicator
550 - fracs: list of tick positions (rational numbers, e.g. instances of frac)
551 possible positions are these tick positions and arbitrary divisions
552 and multiplications by the shift value"""
554 def __init__(self, shift, *fracs):
555 "create a shiftfracs instance and store its shift and fracs information"
556 self.shift = shift
557 self.fracs = fracs
560 class logpart(linpart):
561 """logarithmic partition scheme
562 ticks and label positions are explicitly provided to the constructor"""
564 __implements__ = _Ipart
566 shift5fracs1 = shiftfracs(100000, frac(1, 1))
567 shift4fracs1 = shiftfracs(10000, frac(1, 1))
568 shift3fracs1 = shiftfracs(1000, frac(1, 1))
569 shift2fracs1 = shiftfracs(100, frac(1, 1))
570 shiftfracs1 = shiftfracs(10, frac(1, 1))
571 shiftfracs125 = shiftfracs(10, frac(1, 1), frac(2, 1), frac(5, 1))
572 shiftfracs1to9 = shiftfracs(10, *map(lambda x: frac(x, 1), range(1, 10)))
573 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
575 def __init__(self, ticks=None, labels=None, texts=None, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
576 """configuration of the partition scheme
577 - ticks and labels should be a sequence, where the first value
578 is a shiftfracs instance describing ticks with ticklevel/labellevel 0,
579 the second sequence for ticklevel/labellevel 1, etc.
580 - when the maximum ticklevel/labellevel is 0, just a single
581 shiftfracs instance might be provided in ticks and labels
582 - when labels is None and ticks is not None, the tick entries
583 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
584 - texts are applied to the resulting partition via the
585 mergetexts function (additional information available there)
586 - extendtick allows for the extension of the range given to the
587 defaultpart method to include the next tick with the specified
588 level (None turns off this feature); note, that this feature is
589 also disabled, when an axis prohibits its range extension by
590 the extendmin/extendmax variables given to the defaultpart method
591 - extendlabel is analogous to extendtick, but for labels
592 - epsilon allows for exceeding the axis range by this relative
593 logarithm value (relative to the logarithm axis range given
594 to the defaultpart method) without creating another tick
595 specified by extendtick/extendlabel
596 - mix specifies another partition to be merged into the
597 created partition"""
598 if ticks is None and labels is not None:
599 self.ticks = (helper.ensuresequence(labels)[0],)
600 else:
601 self.ticks = helper.ensuresequence(ticks)
603 if labels is None and ticks is not None:
604 self.labels = (helper.ensuresequence(ticks)[0],)
605 else:
606 self.labels = helper.ensuresequence(labels)
607 self.labels = labels
608 self.extendtick = extendtick
609 self.extendlabel = extendlabel
610 self.epsilon = epsilon
611 self.mix = mix
613 def extendminmax(self, min, max, shiftfracs, extendmin, extendmax):
614 """return new min, max tuple extending the range min, max
615 shiftfracs describes the allowed tick positions
616 extendmin and extendmax are booleans to allow for the extension"""
617 minpower = None
618 maxpower = None
619 for i in xrange(len(shiftfracs.fracs)):
620 imin = int(math.floor(math.log(min / float(shiftfracs.fracs[i])) /
621 math.log(shiftfracs.shift) + self.epsilon)) + 1
622 imax = int(math.ceil(math.log(max / float(shiftfracs.fracs[i])) /
623 math.log(shiftfracs.shift) - self.epsilon)) - 1
624 if minpower is None or imin < minpower:
625 minpower, minindex = imin, i
626 if maxpower is None or imax >= maxpower:
627 maxpower, maxindex = imax, i
628 if minindex:
629 minfrac = shiftfracs.fracs[minindex - 1]
630 else:
631 minfrac = shiftfracs.fracs[-1]
632 minpower -= 1
633 if maxindex != len(shiftfracs.fracs) - 1:
634 maxfrac = shiftfracs.fracs[maxindex + 1]
635 else:
636 maxfrac = shiftfracs.fracs[0]
637 maxpower += 1
638 if extendmin:
639 min = float(minfrac) * float(shiftfracs.shift) ** minpower
640 if extendmax:
641 max = float(maxfrac) * float(shiftfracs.shift) ** maxpower
642 return min, max
644 def getticks(self, min, max, shiftfracs, ticklevel=None, labellevel=None):
645 """return a list of ticks
646 - shiftfracs describes the allowed tick positions
647 - the ticklevel of the ticks is set to ticklevel and
648 the labellevel is set to labellevel
649 - min, max is the range where ticks should be placed"""
650 ticks = list(self.mix)
651 minimin = 0
652 maximax = 0
653 for f in shiftfracs.fracs:
654 fracticks = []
655 imin = int(math.ceil(math.log(min / float(f)) /
656 math.log(shiftfracs.shift) - 0.5 * self.epsilon))
657 imax = int(math.floor(math.log(max / float(f)) /
658 math.log(shiftfracs.shift) + 0.5 * self.epsilon))
659 for i in range(imin, imax + 1):
660 pos = f * frac(shiftfracs.shift, 1, i)
661 fracticks.append(tick(pos.enum, pos.denom, ticklevel = ticklevel, labellevel = labellevel))
662 ticks = _mergeticklists(ticks, fracticks)
663 return ticks
666 class autologpart(logpart):
667 """automatic logarithmic partition scheme
668 possible tick positions are explicitly provided to the constructor"""
670 __implements__ = _Ipart
672 defaultlist = (((logpart.shiftfracs1, # ticks
673 logpart.shiftfracs1to9), # subticks
674 (logpart.shiftfracs1, # labels
675 logpart.shiftfracs125)), # sublevels
677 ((logpart.shiftfracs1, # ticks
678 logpart.shiftfracs1to9), # subticks
679 None), # labels like ticks
681 ((logpart.shift2fracs1, # ticks
682 logpart.shiftfracs1), # subticks
683 None), # labels like ticks
685 ((logpart.shift3fracs1, # ticks
686 logpart.shiftfracs1), # subticks
687 None), # labels like ticks
689 ((logpart.shift4fracs1, # ticks
690 logpart.shiftfracs1), # subticks
691 None), # labels like ticks
693 ((logpart.shift5fracs1, # ticks
694 logpart.shiftfracs1), # subticks
695 None)) # labels like ticks
697 def __init__(self, list=defaultlist, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
698 """configuration of the partition scheme
699 - list should be a sequence of pairs of sequences of shiftfracs
700 instances
701 - within each pair the first sequence contains shiftfracs, where
702 the first shiftfracs instance describes ticks positions with
703 ticklevel 0, the second shiftfracs for ticklevel 1, etc.
704 - the second sequence within each pair describes the same as
705 before, but for labels
706 - within each pair: when the second entry (for the labels) is None
707 and the first entry (for the ticks) ticks is not None, the tick
708 entries for ticklevel 0 are used for labels and vice versa
709 (ticks<->labels)
710 - extendtick allows for the extension of the range given to the
711 defaultpart method to include the next tick with the specified
712 level (None turns off this feature); note, that this feature is
713 also disabled, when an axis prohibits its range extension by
714 the extendmin/extendmax variables given to the defaultpart method
715 - extendlabel is analogous to extendtick, but for labels
716 - epsilon allows for exceeding the axis range by this relative
717 logarithm value (relative to the logarithm axis range given
718 to the defaultpart method) without creating another tick
719 specified by extendtick/extendlabel
720 - mix specifies another partition to be merged into the
721 created partition"""
722 self.list = list
723 if len(list) > 2:
724 self.listindex = divmod(len(list), 2)[0]
725 else:
726 self.listindex = 0
727 self.extendtick = extendtick
728 self.extendlabel = extendlabel
729 self.epsilon = epsilon
730 self.mix = mix
732 def defaultpart(self, min, max, extendmin, extendmax):
733 "method is part of the implementation of _Ipart"
734 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
735 self.morelistindex = self.listindex
736 self.lesslistindex = self.listindex
737 part = logpart(ticks=self.list[self.listindex][0], labels=self.list[self.listindex][1],
738 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
739 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
741 def lesspart(self):
742 "method is part of the implementation of _Ipart"
743 self.lesslistindex += 1
744 if self.lesslistindex < len(self.list):
745 part = logpart(ticks=self.list[self.lesslistindex][0], labels=self.list[self.lesslistindex][1],
746 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
747 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
749 def morepart(self):
750 "method is part of the implementation of _Ipart"
751 self.morelistindex -= 1
752 if self.morelistindex >= 0:
753 part = logpart(ticks=self.list[self.morelistindex][0], labels=self.list[self.morelistindex][1],
754 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
755 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
759 ################################################################################
760 # rater
761 # conseptional remarks:
762 # - raters are used to calculate a rating for a realization of something
763 # - here, a rating means a positive floating point value
764 # - ratings are used to order those realizations by their suitability (lower
765 # ratings are better)
766 # - a rating of None means not suitable at all (those realizations should be
767 # thrown out)
768 ################################################################################
771 class cuberate:
772 """a cube rater
773 - a cube rater has an optimal value, where the rate becomes zero
774 - for a left (below the optimum) and a right value (above the optimum),
775 the rating is value is set to 1 (modified by an overall weight factor
776 for the rating)
777 - the analytic form of the rating is cubic for both, the left and
778 the right side of the rater, independently"""
780 def __init__(self, opt, left=None, right=None, weight=1):
781 """initializes the rater
782 - by default, left is set to zero, right is set to 3*opt
783 - left should be smaller than opt, right should be bigger than opt
784 - weight should be positive and is a factor multiplicated to the rates"""
785 if left is None:
786 left = 0
787 if right is None:
788 right = 3*opt
789 self.opt = opt
790 self.left = left
791 self.right = right
792 self.weight = weight
794 def rate(self, value, dense=1):
795 """returns a rating for a value
796 - the dense factor lineary rescales the rater (the optimum etc.),
797 e.g. a value bigger than one increases the optimum (when it is
798 positive) and a value lower than one decreases the optimum (when
799 it is positive); the dense factor itself should be positive"""
800 opt = self.opt * dense
801 if value < opt:
802 other = self.left * dense
803 elif value > opt:
804 other = self.right * dense
805 else:
806 return 0
807 factor = (value - opt) / float(other - opt)
808 return self.weight * (factor ** 3)
811 class distancerate:
812 """a distance rater (rates a list of distances)
813 - the distance rater rates a list of distances by rating each independently
814 and returning the average rate
815 - there is an optimal value, where the rate becomes zero
816 - the analytic form is linary for values above the optimal value
817 (twice the optimal value has the rating one, three times the optimal
818 value has the rating two, etc.)
819 - the analytic form is reciprocal subtracting one for values below the
820 optimal value (halve the optimal value has the rating one, one third of
821 the optimal value has the rating two, etc.)"""
823 def __init__(self, opt, weight=0.1):
824 """inititializes the rater
825 - opt is the optimal length (a PyX length, by default a visual length)
826 - weight should be positive and is a factor multiplicated to the rates"""
827 self.opt_str = opt
828 self.weight = weight
830 def _rate(self, distances, dense=1):
831 """rate distances
832 - the distances are a sequence of positive floats in PostScript points
833 - the dense factor lineary rescales the rater (the optimum etc.),
834 e.g. a value bigger than one increases the optimum (when it is
835 positive) and a value lower than one decreases the optimum (when
836 it is positive); the dense factor itself should be positive"""
837 if len(distances):
838 opt = unit.topt(unit.length(self.opt_str, default_type="v")) / dense
839 rate = 0
840 for distance in distances:
841 if distance < opt:
842 rate += self.weight * (opt / distance - 1)
843 else:
844 rate += self.weight * (distance / opt - 1)
845 return rate / float(len(distances))
848 class axisrater:
849 """a rater for axis partitions
850 - the rating of axes is splited into two separate parts:
851 - rating of the partitions in terms of the number of ticks,
852 subticks, labels, etc.
853 - rating of the label distances
854 - in the end, a rate for an axis partition is the sum of these rates
855 - it is useful to first just rate the number of ticks etc.
856 and selecting those partitions, where this fits well -> as soon
857 as an complete rate (the sum of both parts from the list above)
858 of a first partition is below a rate of just the ticks of another
859 partition, this second partition will never be better than the
860 first one -> we gain speed by minimizing the number of partitions,
861 where label distances have to be taken into account)
862 - both parts of the rating are shifted into instances of raters
863 defined above --- right now, there is not yet a strict interface
864 for this delegation (should be done as soon as it is needed)"""
866 linticks = (cuberate(4), cuberate(10, weight=0.5), )
867 linlabels = (cuberate(4), )
868 logticks = (cuberate(5, right=20), cuberate(20, right=100, weight=0.5), )
869 loglabels = (cuberate(5, right=20), cuberate(5, left=-20, right=20, weight=0.5), )
870 stdtickrange = cuberate(1, weight=2)
871 stddistance = distancerate("1 cm")
873 def __init__(self, ticks=linticks, labels=linlabels, tickrange=stdtickrange, distance=stddistance):
874 """initializes the axis rater
875 - ticks and labels are lists of instances of cuberate
876 - the first entry in ticks rate the number of ticks, the
877 second the number of subticks, etc.; when there are no
878 ticks of a level or there is not rater for a level, the
879 level is just ignored
880 - labels is analogous, but for labels
881 - within the rating, all ticks with a higher level are
882 considered as ticks for a given level
883 - tickrange is a cuberate instance, which rates the covering
884 of an axis range by the ticks (as a relative value of the
885 tick range vs. the axis range), ticks might cover less or
886 more than the axis range (for the standard automatic axis
887 partition schemes an extention of the axis range is normal
888 and should get some penalty)
889 - distance is an distancerate instance"""
890 self.ticks = ticks
891 self.labels = labels
892 self.tickrange = tickrange
893 self.distance = distance
895 def ratepart(self, axis, part, dense=1):
896 """rates a partition by some global parameters
897 - takes into account the number of ticks, subticks, etc.,
898 number of labels, etc., and the coverage of the axis
899 range by the ticks
900 - when there are no ticks of a level or there was not rater
901 given in the constructor for a level, the level is just
902 ignored
903 - the method returns the sum of the rating results divided
904 by the sum of the weights of the raters
905 - within the rating, all ticks with a higher level are
906 considered as ticks for a given level"""
907 tickslen = len(self.ticks)
908 labelslen = len(self.labels)
909 ticks = [0]*tickslen
910 labels = [0]*labelslen
911 if part is not None:
912 for tick in part:
913 if tick.ticklevel is not None:
914 for level in xrange(tick.ticklevel, tickslen):
915 ticks[level] += 1
916 if tick.labellevel is not None:
917 for level in xrange(tick.labellevel, labelslen):
918 labels[level] += 1
919 rate = 0
920 weight = 0
921 for tick, rater in zip(ticks, self.ticks):
922 rate += rater.rate(tick, dense=dense)
923 weight += rater.weight
924 for label, rater in zip(labels, self.labels):
925 rate += rater.rate(label, dense=dense)
926 weight += rater.weight
927 if part is not None and len(part):
928 tickmin, tickmax = axis.gettickrange() # XXX: tickrange was not yet applied!?
929 rate += self.tickrange.rate((float(part[-1]) - float(part[0])) * axis.divisor / (tickmax - tickmin))
930 else:
931 rate += self.tickrange.rate(0)
932 weight += self.tickrange.weight
933 return rate/weight
935 def _ratedistances(self, distances, dense=1):
936 """rate distances
937 - the distances should be collected as box distances of
938 subsequent labels (of any level))
939 - the distances are a sequence of positive floats in
940 PostScript points
941 - the dense factor is used within the distancerate instance"""
942 return self.distance._rate(distances, dense=dense)
945 ################################################################################
946 # texter
947 # texter automatically create labels for tick instances
948 ################################################################################
951 class _Itexter:
953 def labels(self, ticks):
954 """fill the label attribute of ticks
955 - ticks is a list of instances of tick
956 - for each element of ticks the value of the attribute label is set to
957 a string appropriate to the attributes enum and denom of that tick
958 instance
959 - label attributes of the tick instances are just kept, whenever they
960 are not equal to None
961 - the method might add texsetting instances to the labelattrs attribute
962 of the ticks"""
965 class notexter:
966 """a texter, which does just nothing (I'm not sure, if this makes sense)"""
968 __implements__ = _Itexter
970 def labels(self, ticks):
971 pass
974 class rationaltexter:
975 """a texter creating rational labels (e.g. "a/b" or even "a \over b")"""
976 # XXX: we use divmod here to be more portable
978 __implements__ = _Itexter
980 def __init__(self, prefix="", suffix="",
981 enumprefix="", enumsuffix="",
982 denomprefix="", denomsuffix="",
983 equaldenom=0, minuspos=0, over=r"{{%s}\over{%s}}",
984 skipenum0=1, skipenum1=1, skipdenom1=1,
985 labelattrs=textmodule.mathmode):
986 """initializes the instance
987 - prefix and suffix (strings) are just added to the begin and to
988 the end of the label, respectively
989 - prefixenum and suffixenum (strings) are added to the labels
990 enumerator simularly
991 - prefixdenom and suffixdenom (strings) are added to the labels
992 denominator simularly
993 - the enumerator and denominator are +++TODO: KÜRZEN+++; however,
994 when equaldenom is set, the least common multiple of all
995 denominators is used
996 - minuspos is an integer, which determines the position, where the
997 minus sign has to be placed; the following values are allowed:
998 0 - written immediately before the prefix
999 1 - written immediately after the prefix
1000 2 - written immediately before the enumprefix
1001 3 - written immediately after the enumprefix
1002 4 - written immediately before the denomprefix
1003 5 - written immediately after the denomprefix
1004 - over (string) is taken as a format string generating the
1005 fraction bar; it has to contain exactly two string insert
1006 operators "%s" - the first for the enumerator and the second
1007 for the denominator; by far the most common examples are
1008 r"{{%s}\over{%s}}" and "{{%s}/{%s}}"
1009 - skipenum0 (boolean) just prints a zero instead of
1010 the hole fraction, when the enumerator is zero;
1011 all prefixes and suffixes are omitted as well
1012 - skipenum1 (boolean) just prints the enumprefix directly followed
1013 by the enumsuffix, when the enum value is one and at least either,
1014 the enumprefix or the enumsuffix is present
1015 - skipdenom1 (boolean) just prints the enumerator instead of
1016 the hole fraction, when the denominator is one
1017 - labelattrs is a sequence of texsetting instances; also just a single
1018 instance is allowed; an empty sequence is allowed as well, but None
1019 is not valid"""
1020 self.prefix = prefix
1021 self.suffix = suffix
1022 self.enumprefix = enumprefix
1023 self.enumsuffix = enumsuffix
1024 self.denomprefix = denomprefix
1025 self.denomsuffix = denomsuffix
1026 self.equaldenom = equaldenom
1027 self.minuspos = minuspos
1028 if self.minuspos < 0 or self.minuspos > 5:
1029 raise RuntimeError("invalid minuspos")
1030 self.over = over
1031 self.skipenum0 = skipenum0
1032 self.skipenum1 = skipenum1
1033 self.skipdenom1 = skipdenom1
1034 self.labelattrs = helper.ensuresequence(labelattrs)
1036 def gcd(self, m, n):
1037 """returns the greates common divisor
1038 - m and n must be non-negative integers"""
1039 if m < n:
1040 m, n = n, m
1041 while n > 0:
1042 m, (dummy, n) = n, divmod(m, n) # ensure portable integer division
1043 return m
1045 def lcm(self, *n):
1046 """returns the least common multiple of all elements in n
1047 - the elements of n must be integers
1048 - return None if the number of elements is zero"""
1049 "TODO: fill it from knuth"
1050 pass
1052 def labels(self, ticks):
1053 # the temporary variables fracenum, fracdenom, and fracminus are
1054 # inserted into all tick instances, where label is not None
1055 for tick in ticks:
1056 if tick.label is None:
1057 tick.fracminus = 1
1058 tick.fracenum = tick.enum
1059 tick.fracdenom = tick.denom
1060 if tick.fracenum < 0:
1061 tick.fracminus *= -1
1062 tick.fracenum *= -1
1063 if tick.fracdenom < 0:
1064 tick.fracminus *= -1
1065 tick.fracdenom *= -1
1066 gcd = self.gcd(tick.fracenum, tick.fracdenom)
1067 (tick.fracenum, dummy1), (tick.fracdenom, dummy2) = divmod(tick.enum, gcd), divmod(tick.fracdenom, gcd)
1068 if self.equaldenom:
1069 equaldenom = self.lcm([tick.fracdenom for tick in ticks if tick.label is None])
1070 if equaldenom is not None:
1071 for tick in ticks:
1072 if tick.label is None:
1073 factor, dummy = divmod(equaldenom, tick.fracdenom)
1074 assert dummy != 0, "internal error: wrong lowest common multiple?" # TODO: remove that check
1075 tick.fracenum, tick.fracdenom = factor * tick.fracenum, factor * tick.fracdenom
1076 for tick in ticks:
1077 if tick.label is None:
1078 if tick.fracminus == -1:
1079 tick.fracminus = "-"
1080 else:
1081 tick.fracminus = ""
1082 if self.skipenum0 and tick.fracenum == 0:
1083 if self.minuspos == 2 or self.minuspos == 3:
1084 tick.fracenum = "%s0" % tick.fracminus
1085 else:
1086 tick.fracenum = tick.fracenum
1087 elif self.skipenum1 and tick.fracenum == 1 and (len(self.enumprefix) or len(self.enumsuffix)):
1088 if self.minuspos == 2:
1089 tick.fracenum = "%s%s%s" % (tick.fracminus, self.enumprefix, self.enumsuffix)
1090 elif self.minuspos == 3:
1091 tick.fracenum = "%s%s%s" % (self.enumprefix, tick.fracminus, self.enumsuffix)
1092 else:
1093 tick.fracenum = "%s%s" % (self.enumprefix, self.enumsuffix)
1094 else:
1095 if self.minuspos == 2:
1096 tick.fracenum = "%s%s%i%s" % (tick.fracminus, self.enumprefix, tick.fracenum, self.enumsuffix)
1097 elif self.minuspos == 3:
1098 tick.fracenum = "%s%s%i%s" % (self.enumprefix, tick.fracminus, tick.fracenum, self.enumsuffix)
1099 else:
1100 tick.fracenum = "%s%i%s" % (self.enumprefix, tick.fracenum, self.enumsuffix)
1101 if self.skipdenom1 and tick.fracdenom == 1 and self.minuspos != 4 and self.minuspos != 5:
1102 frac = tick.fracenum
1103 else:
1104 if self.minuspos == 4:
1105 tick.fracdenom = "%s%s%i%s" % (tick.fracminus, self.denomprefix, tick.fracdenom, self.denomsuffix)
1106 elif self.minuspos == 5:
1107 tick.fracdenom = "%s%s%i%s" % (self.denomprefix, tick.fracminus, tick.fracdenom, self.denomsuffix)
1108 else:
1109 tick.fracdenom = "%s%i%s" % (self.denomprefix, tick.fracdenom, self.denomsuffix)
1110 frac = self.over % (tick.fracenum, tick.fracdenom)
1111 if self.minuspos == 0:
1112 tick.label = "%s%s%s%s" % (tick.fracminus, self.prefix, frac, self.suffix)
1113 elif self.minuspos == 1:
1114 tick.label = "%s%s%s%s" % (self.prefix, tick.fracminus, frac, self.suffix)
1115 else:
1116 tick.label = "%s%s%s" % (self.prefix, frac, self.suffix)
1120 ################################################################################
1121 # axis painter
1122 ################################################################################
1125 class layoutdata: pass
1128 class axistitlepainter:
1130 paralleltext = -90
1131 orthogonaltext = 0
1133 def __init__(self, titledist="0.3 cm",
1134 titleattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1135 titledirection=-90,
1136 titlepos=0.5):
1137 self.titledist_str = titledist
1138 self.titleattrs = titleattrs
1139 self.titledirection = titledirection
1140 self.titlepos = titlepos
1142 def reldirection(self, direction, dx, dy, epsilon=1e-10):
1143 direction += math.atan2(dy, dx) * 180 / math.pi
1144 while (direction > 90 + epsilon):
1145 direction -= 180
1146 while (direction < -90 - epsilon):
1147 direction += 180
1148 return direction
1150 def dolayout(self, graph, axis):
1151 if axis.title is not None and self.titleattrs is not None:
1152 titledist = unit.topt(unit.length(self.titledist_str, default_type="v"))
1153 x, y = axis._vtickpoint(axis, self.titlepos)
1154 dx, dy = axis.vtickdirection(axis, self.titlepos)
1155 # no not modify self.titleattrs ... the painter might be used by several axes!!!
1156 titleattrs = list(helper.ensuresequence(self.titleattrs))
1157 if self.titledirection is not None:
1158 titleattrs = titleattrs + [trafo.rotate(self.reldirection(self.titledirection, dx, dy))]
1159 axis.layoutdata.titlebox = graph.texrunner._text(x, y, axis.title, *titleattrs)
1160 axis.layoutdata._extent += titledist
1161 axis.layoutdata.titlebox._linealign(axis.layoutdata._extent, dx, dy)
1162 axis.layoutdata._extent += axis.layoutdata.titlebox._extent(dx, dy)
1163 else:
1164 axis.layoutdata.titlebox = None
1166 def paint(self, graph, axis):
1167 if axis.layoutdata.titlebox is not None:
1168 graph.insert(axis.layoutdata.titlebox)
1171 class axispainter(axistitlepainter):
1173 defaultticklengths = ["%0.5f cm" % (0.2*goldenmean**(-i)) for i in range(10)]
1175 fractypeauto = 1
1176 fractyperat = 2
1177 fractypedec = 3
1178 fractypeexp = 4
1180 def __init__(self, innerticklengths=defaultticklengths,
1181 outerticklengths=None,
1182 tickattrs=(),
1183 gridattrs=None,
1184 zerolineattrs=(),
1185 baselineattrs=canvas.linecap.square,
1186 labeldist="0.3 cm",
1187 labelattrs=((textmodule.halign.center, textmodule.vshift.mathaxis),
1188 (textmodule.size.footnotesize, textmodule.halign.center, textmodule.vshift.mathaxis)),
1189 labeldirection=None,
1190 labelhequalize=0,
1191 labelvequalize=1,
1192 fractype=fractypeauto,
1193 ratfracsuffixenum=1,
1194 ratfracover=r"\over",
1195 decfracpoint=".",
1196 decfracequal=0,
1197 expfractimes=r"\cdot",
1198 expfracpre1=0,
1199 expfracminexp=4,
1200 suffix0=0,
1201 suffix1=0,
1202 **args):
1203 self.innerticklengths_str = innerticklengths
1204 self.outerticklengths_str = outerticklengths
1205 self.tickattrs = tickattrs
1206 self.gridattrs = gridattrs
1207 self.zerolineattrs = zerolineattrs
1208 self.baselineattrs = baselineattrs
1209 self.labeldist_str = labeldist
1210 self.labelattrs = labelattrs
1211 self.labeldirection = labeldirection
1212 self.labelhequalize = labelhequalize
1213 self.labelvequalize = labelvequalize
1214 self.fractype = fractype
1215 self.ratfracsuffixenum = ratfracsuffixenum
1216 self.ratfracover = ratfracover
1217 self.decfracpoint = decfracpoint
1218 self.decfracequal = decfracequal
1219 self.expfractimes = expfractimes
1220 self.expfracpre1 = expfracpre1
1221 self.expfracminexp = expfracminexp
1222 self.suffix0 = suffix0
1223 self.suffix1 = suffix1
1224 axistitlepainter.__init__(self, **args)
1226 def attachsuffix(self, tick, str):
1227 if self.suffix0 or tick.enum:
1228 if tick.suffix is not None and not self.suffix1:
1229 if str == "1":
1230 str = ""
1231 elif str == "-1":
1232 str = "-"
1233 if tick.suffix is not None:
1234 str = str + tick.suffix
1235 return str
1237 def ratfrac(self, tick):
1238 m, n = tick.enum, tick.denom
1239 sign = 1
1240 if m < 0: m, sign = -m, -sign
1241 if n < 0: n, sign = -n, -sign
1242 gcd = self.gcd(m, n)
1243 (m, dummy1), (n, dummy2) = divmod(m, gcd), divmod(n, gcd)
1244 if n != 1:
1245 if self.ratfracsuffixenum:
1246 if sign == -1:
1247 return "-{{%s}%s{%s}}" % (self.attachsuffix(tick, str(m)), self.ratfracover, n)
1248 else:
1249 return "{{%s}%s{%s}}" % (self.attachsuffix(tick, str(m)), self.ratfracover, n)
1250 else:
1251 if sign == -1:
1252 return self.attachsuffix(tick, "-{{%s}%s{%s}}" % (m, self.ratfracover, n))
1253 else:
1254 return self.attachsuffix(tick, "{{%s}%s{%s}}" % (m, self.ratfracover, n))
1255 else:
1256 if sign == -1:
1257 return self.attachsuffix(tick, "-%s" % m)
1258 else:
1259 return self.attachsuffix(tick, "%s" % m)
1261 def decfrac(self, tick, decfraclength=None):
1262 m, n = tick.enum, tick.denom
1263 sign = 1
1264 if m < 0: m, sign = -m, -sign
1265 if n < 0: n, sign = -n, -sign
1266 gcd = self.gcd(m, n)
1267 (m, dummy1), (n, dummy2) = divmod(m, gcd), divmod(n, gcd)
1268 frac, rest = divmod(m, n)
1269 strfrac = str(frac)
1270 rest = m % n
1271 if rest:
1272 strfrac += self.decfracpoint
1273 oldrest = []
1274 tick.decfraclength = 0
1275 while (rest):
1276 tick.decfraclength += 1
1277 if rest in oldrest:
1278 periodstart = len(strfrac) - (len(oldrest) - oldrest.index(rest))
1279 strfrac = strfrac[:periodstart] + r"\overline{" + strfrac[periodstart:] + "}"
1280 break
1281 oldrest += [rest]
1282 rest *= 10
1283 frac, rest = divmod(rest, n)
1284 strfrac += str(frac)
1285 else:
1286 if decfraclength is not None:
1287 while tick.decfraclength < decfraclength:
1288 strfrac += "0"
1289 tick.decfraclength += 1
1290 if sign == -1:
1291 return self.attachsuffix(tick, "-%s" % strfrac)
1292 else:
1293 return self.attachsuffix(tick, strfrac)
1295 def expfrac(self, tick, minexp = None):
1296 m, n = tick.enum, tick.denom
1297 sign = 1
1298 if m < 0: m, sign = -m, -sign
1299 if n < 0: n, sign = -n, -sign
1300 exp = 0
1301 if m:
1302 while divmod(m, n)[0] > 9:
1303 n *= 10
1304 exp += 1
1305 while divmod(m, n)[0] < 1:
1306 m *= 10
1307 exp -= 1
1308 if minexp is not None and ((exp < 0 and -exp < minexp) or (exp >= 0 and exp < minexp)):
1309 return None
1310 dummy = frac(m, n)
1311 dummy.suffix = None
1312 prefactor = self.decfrac(dummy)
1313 if prefactor == "1" and not self.expfracpre1:
1314 if sign == -1:
1315 return self.attachsuffix(tick, "-10^{%i}" % exp)
1316 else:
1317 return self.attachsuffix(tick, "10^{%i}" % exp)
1318 else:
1319 if sign == -1:
1320 return self.attachsuffix(tick, "-%s%s10^{%i}" % (prefactor, self.expfractimes, exp))
1321 else:
1322 return self.attachsuffix(tick, "%s%s10^{%i}" % (prefactor, self.expfractimes, exp))
1324 def createtext(self, tick):
1325 tick.decfraclength = None
1326 if self.fractype == self.fractypeauto:
1327 if tick.suffix is not None:
1328 tick.label = self.ratfrac(tick)
1329 else:
1330 tick.label = self.expfrac(tick, self.expfracminexp)
1331 if tick.label is None:
1332 tick.label = self.decfrac(tick)
1333 elif self.fractype == self.fractypedec:
1334 tick.label = self.decfrac(tick)
1335 elif self.fractype == self.fractypeexp:
1336 tick.label = self.expfrac(tick)
1337 elif self.fractype == self.fractyperat:
1338 tick.label = self.ratfrac(tick)
1339 else:
1340 raise ValueError("fractype invalid")
1341 if textmodule.mathmode not in helper.getattrs(tick.labelattrs, textmodule._texsetting, []):
1342 tick.labelattrs.append(textmodule.mathmode)
1344 def dolayout(self, graph, axis):
1345 labeldist = unit.topt(unit.length(self.labeldist_str, default_type="v"))
1346 for tick in axis.ticks:
1347 tick.virtual = axis.convert(float(tick) * axis.divisor)
1348 tick.x, tick.y = axis._vtickpoint(axis, tick.virtual)
1349 tick.dx, tick.dy = axis.vtickdirection(axis, tick.virtual)
1350 for tick in axis.ticks:
1351 tick.textbox = None
1352 if tick.labellevel is not None:
1353 tick.labelattrs = helper.getsequenceno(self.labelattrs, tick.labellevel)
1354 if tick.labelattrs is not None:
1355 tick.labelattrs = list(helper.ensuresequence(tick.labelattrs))
1356 if tick.label is None:
1357 tick.suffix = axis.suffix
1358 self.createtext(tick)
1359 if self.labeldirection is not None:
1360 tick.labelattrs += [trafo.rotate(self.reldirection(self.labeldirection, tick.dx, tick.dy))]
1361 tick.textbox = textmodule._text(tick.x, tick.y, tick.label, *tick.labelattrs)
1362 if self.decfracequal:
1363 maxdecfraclength = max([tick.decfraclength for tick in axis.ticks if tick.labellevel is not None and
1364 tick.labelattrs is not None and
1365 tick.decfraclength is not None])
1366 for tick in axis.ticks:
1367 if (tick.labellevel is not None and
1368 tick.labelattrs is not None and
1369 tick.decfraclength is not None):
1370 tick.label = self.decfrac(tick, maxdecfraclength)
1371 for tick in axis.ticks:
1372 if tick.labellevel is not None and tick.labelattrs is not None:
1373 tick.textbox = textmodule._text(tick.x, tick.y, tick.label, *tick.labelattrs)
1374 if len(axis.ticks) > 1:
1375 equaldirection = 1
1376 for tick in axis.ticks[1:]:
1377 if tick.dx != axis.ticks[0].dx or tick.dy != axis.ticks[0].dy:
1378 equaldirection = 0
1379 else:
1380 equaldirection = 0
1381 if equaldirection and ((not axis.ticks[0].dx and self.labelvequalize) or
1382 (not axis.ticks[0].dy and self.labelhequalize)):
1383 box._linealignequal([tick.textbox for tick in axis.ticks if tick.textbox],
1384 labeldist, axis.ticks[0].dx, axis.ticks[0].dy)
1385 else:
1386 for tick in axis.ticks:
1387 if tick.textbox:
1388 tick.textbox._linealign(labeldist, tick.dx, tick.dy)
1389 def topt_v_recursive(arg):
1390 if helper.issequence(arg):
1391 # return map(topt_v_recursive, arg) needs python2.2
1392 return [unit.topt(unit.length(a, default_type="v")) for a in arg]
1393 else:
1394 if arg is not None:
1395 return unit.topt(unit.length(arg, default_type="v"))
1396 innerticklengths = topt_v_recursive(self.innerticklengths_str)
1397 outerticklengths = topt_v_recursive(self.outerticklengths_str)
1398 axis.layoutdata._extent = 0
1399 for tick in axis.ticks:
1400 if tick.ticklevel is not None:
1401 tick.innerticklength = helper.getitemno(innerticklengths, tick.ticklevel)
1402 tick.outerticklength = helper.getitemno(outerticklengths, tick.ticklevel)
1403 if tick.innerticklength is not None and tick.outerticklength is None:
1404 tick.outerticklength = 0
1405 if tick.outerticklength is not None and tick.innerticklength is None:
1406 tick.innerticklength = 0
1407 extent = 0
1408 if tick.textbox is None:
1409 if tick.outerticklength is not None and tick.outerticklength > 0:
1410 extent = tick.outerticklength
1411 else:
1412 extent = tick.textbox._extent(tick.dx, tick.dy) + labeldist
1413 if axis.layoutdata._extent < extent:
1414 axis.layoutdata._extent = extent
1415 axistitlepainter.dolayout(self, graph, axis)
1417 def ratelayout(self, graph, axis, dense=1):
1418 ticktextboxes = [tick.textbox for tick in axis.ticks if tick.textbox is not None]
1419 if len(ticktextboxes) > 1:
1420 try:
1421 distances = [ticktextboxes[i]._boxdistance(ticktextboxes[i+1]) for i in range(len(ticktextboxes) - 1)]
1422 except box.BoxCrossError:
1423 return None
1424 rate = axis.rate._ratedistances(distances, dense)
1425 return rate
1426 else:
1427 if self.labelattrs is None:
1428 return 0
1430 def paint(self, graph, axis):
1431 for tick in axis.ticks:
1432 if tick.ticklevel is not None:
1433 if tick != frac(0, 1) or self.zerolineattrs is None:
1434 gridattrs = helper.getsequenceno(self.gridattrs, tick.ticklevel)
1435 if gridattrs is not None:
1436 graph.stroke(axis.vgridpath(tick.virtual), *helper.ensuresequence(gridattrs))
1437 tickattrs = helper.getsequenceno(self.tickattrs, tick.ticklevel)
1438 if None not in (tick.innerticklength, tick.outerticklength, tickattrs):
1439 x1 = tick.x - tick.dx * tick.innerticklength
1440 y1 = tick.y - tick.dy * tick.innerticklength
1441 x2 = tick.x + tick.dx * tick.outerticklength
1442 y2 = tick.y + tick.dy * tick.outerticklength
1443 graph.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(tickattrs))
1444 if tick.textbox is not None:
1445 graph.insert(tick.textbox)
1446 if self.baselineattrs is not None:
1447 graph.stroke(axis.vbaseline(axis), *helper.ensuresequence(self.baselineattrs))
1448 if self.zerolineattrs is not None:
1449 if len(axis.ticks) and axis.ticks[0] * axis.ticks[-1] < frac(0, 1):
1450 graph.stroke(axis.vgridpath(axis.convert(0)), *helper.ensuresequence(self.zerolineattrs))
1451 axistitlepainter.paint(self, graph, axis)
1454 class splitaxispainter(axistitlepainter):
1456 def __init__(self, breaklinesdist=0.05,
1457 breaklineslength=0.5,
1458 breaklinesangle=-60,
1459 breaklinesattrs=(),
1460 **args):
1461 self.breaklinesdist_str = breaklinesdist
1462 self.breaklineslength_str = breaklineslength
1463 self.breaklinesangle = breaklinesangle
1464 self.breaklinesattrs = breaklinesattrs
1465 axistitlepainter.__init__(self, **args)
1467 def subvbaseline(self, axis, v1=None, v2=None):
1468 if v1 is None:
1469 if self.breaklinesattrs is None:
1470 left = axis.vmin
1471 else:
1472 if axis.vminover is None:
1473 left = None
1474 else:
1475 left = axis.vminover
1476 else:
1477 left = axis.vmin+v1*(axis.vmax-axis.vmin)
1478 if v2 is None:
1479 if self.breaklinesattrs is None:
1480 right = axis.vmax
1481 else:
1482 if axis.vmaxover is None:
1483 right = None
1484 else:
1485 right = axis.vmaxover
1486 else:
1487 right = axis.vmin+v2*(axis.vmax-axis.vmin)
1488 return axis.baseaxis.vbaseline(axis.baseaxis, left, right)
1490 def dolayout(self, graph, axis):
1491 if self.breaklinesattrs is not None:
1492 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1493 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1494 self._breaklinesdist = unit.topt(self.breaklinesdist)
1495 self._breaklineslength = unit.topt(self.breaklineslength)
1496 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1497 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1498 axis.layoutdata._extent = (math.fabs(0.5 * self._breaklinesdist * self.cos) +
1499 math.fabs(0.5 * self._breaklineslength * self.sin))
1500 else:
1501 axis.layoutdata._extent = 0
1502 for subaxis in axis.axislist:
1503 subaxis.baseaxis = axis
1504 subaxis._vtickpoint = lambda axis, v: axis.baseaxis._vtickpoint(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1505 subaxis.vtickdirection = lambda axis, v: axis.baseaxis.vtickdirection(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1506 subaxis.vbaseline = self.subvbaseline
1507 subaxis.dolayout(graph)
1508 if axis.layoutdata._extent < subaxis.layoutdata._extent:
1509 axis.layoutdata._extent = subaxis.layoutdata._extent
1510 axistitlepainter.dolayout(self, graph, axis)
1512 def paint(self, graph, axis):
1513 for subaxis in axis.axislist:
1514 subaxis.dopaint(graph)
1515 if self.breaklinesattrs is not None:
1516 for subaxis1, subaxis2 in zip(axis.axislist[:-1], axis.axislist[1:]):
1517 # use a tangent of the baseline (this is independent of the tickdirection)
1518 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1519 breakline = path.normpath(axis.vbaseline(axis, v, None)).tangent(0, self.breaklineslength)
1520 widthline = path.normpath(axis.vbaseline(axis, v, None)).tangent(0, self.breaklinesdist).transformed(trafo.rotate(self.breaklinesangle+90, *breakline.begin()))
1521 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1522 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1523 breakline = breakline.transformed(trafo.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1524 breakline1 = breakline.transformed(trafo.translate(*towidth))
1525 breakline2 = breakline.transformed(trafo.translate(-towidth[0], -towidth[1]))
1526 graph.fill(path.path(path.moveto(*breakline1.begin()),
1527 path.lineto(*breakline1.end()),
1528 path.lineto(*breakline2.end()),
1529 path.lineto(*breakline2.begin()),
1530 path.closepath()), color.gray.white)
1531 graph.stroke(breakline1, *helper.ensuresequence(self.breaklinesattrs))
1532 graph.stroke(breakline2, *helper.ensuresequence(self.breaklinesattrs))
1533 axistitlepainter.paint(self, graph, axis)
1536 class baraxispainter(axistitlepainter):
1538 def __init__(self, innerticklength=None,
1539 outerticklength=None,
1540 tickattrs=(),
1541 baselineattrs=canvas.linecap.square,
1542 namedist="0.3 cm",
1543 nameattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1544 namedirection=None,
1545 namepos=0.5,
1546 namehequalize=0,
1547 namevequalize=1,
1548 **args):
1549 self.innerticklength_str = innerticklength
1550 self.outerticklength_str = outerticklength
1551 self.tickattrs = tickattrs
1552 self.baselineattrs = baselineattrs
1553 self.namedist_str = namedist
1554 self.nameattrs = nameattrs
1555 self.namedirection = namedirection
1556 self.namepos = namepos
1557 self.namehequalize = namehequalize
1558 self.namevequalize = namevequalize
1559 axistitlepainter.__init__(self, **args)
1561 def dolayout(self, graph, axis):
1562 axis.layoutdata._extent = 0
1563 if axis.multisubaxis:
1564 for name, subaxis in zip(axis.names, axis.subaxis):
1565 subaxis.vmin = axis.convert((name, 0))
1566 subaxis.vmax = axis.convert((name, 1))
1567 subaxis.baseaxis = axis
1568 subaxis._vtickpoint = lambda axis, v: axis.baseaxis._vtickpoint(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1569 subaxis.vtickdirection = lambda axis, v: axis.baseaxis.vtickdirection(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1570 subaxis.vbaseline = None
1571 subaxis.dolayout(graph)
1572 if axis.layoutdata._extent < subaxis.layoutdata._extent:
1573 axis.layoutdata._extent = subaxis.layoutdata._extent
1574 axis.namepos = []
1575 for name in axis.names:
1576 v = axis.convert((name, self.namepos))
1577 x, y = axis._vtickpoint(axis, v)
1578 dx, dy = axis.vtickdirection(axis, v)
1579 axis.namepos.append((v, x, y, dx, dy))
1580 axis.nameboxes = []
1581 if self.nameattrs is not None:
1582 for (v, x, y, dx, dy), name in zip(axis.namepos, axis.names):
1583 nameattrs = helper.ensurelist(self.nameattrs)
1584 if self.namedirection is not None:
1585 nameattrs += [trafo.rotate(self.reldirection(self.namedirection, dx, dy))]
1586 if axis.texts.has_key(name):
1587 axis.nameboxes.append(textmodule._text(x, y, str(axis.texts[name]), *nameattrs))
1588 elif axis.texts.has_key(str(name)):
1589 axis.nameboxes.append(textmodule._text(x, y, str(axis.texts[str(name)]), *nameattrs))
1590 else:
1591 axis.nameboxes.append(textmodule._text(x, y, str(name), *nameattrs))
1592 labeldist = axis.layoutdata._extent + unit.topt(unit.length(self.namedist_str, default_type="v"))
1593 if len(axis.namepos) > 1:
1594 equaldirection = 1
1595 for namepos in axis.namepos[1:]:
1596 if namepos[3] != axis.namepos[0][3] or namepos[4] != axis.namepos[0][4]:
1597 equaldirection = 0
1598 else:
1599 equaldirection = 0
1600 if equaldirection and ((not axis.namepos[0][3] and self.namevequalize) or
1601 (not axis.namepos[0][4] and self.namehequalize)):
1602 box._linealignequal(axis.nameboxes, labeldist, axis.namepos[0][3], axis.namepos[0][4])
1603 else:
1604 for namebox, namepos in zip(axis.nameboxes, axis.namepos):
1605 namebox._linealign(labeldist, namepos[3], namepos[4])
1606 if self.innerticklength_str is not None:
1607 axis.innerticklength = unit.topt(unit.length(self.innerticklength_str, default_type="v"))
1608 else:
1609 if self.outerticklength_str is not None:
1610 axis.innerticklength = 0
1611 else:
1612 axis.innerticklength = None
1613 if self.outerticklength_str is not None:
1614 axis.outerticklength = unit.topt(unit.length(self.outerticklength_str, default_type="v"))
1615 else:
1616 if self.innerticklength_str is not None:
1617 axis.outerticklength = 0
1618 else:
1619 axis.outerticklength = None
1620 if axis.outerticklength is not None and self.tickattrs is not None:
1621 axis.layoutdata._extent += axis.outerticklength
1622 for (v, x, y, dx, dy), namebox in zip(axis.namepos, axis.nameboxes):
1623 newextent = namebox._extent(dx, dy) + labeldist
1624 if axis.layoutdata._extent < newextent:
1625 axis.layoutdata._extent = newextent
1626 graph.mindbbox(*[namebox.bbox() for namebox in axis.nameboxes])
1627 axistitlepainter.dolayout(self, graph, axis)
1629 def paint(self, graph, axis):
1630 if axis.subaxis is not None:
1631 if axis.multisubaxis:
1632 for subaxis in axis.subaxis:
1633 subaxis.dopaint(graph)
1634 if None not in (self.tickattrs, axis.innerticklength, axis.outerticklength):
1635 for pos in axis.relsizes:
1636 if pos == axis.relsizes[0]:
1637 pos -= axis.firstdist
1638 elif pos != axis.relsizes[-1]:
1639 pos -= 0.5 * axis.dist
1640 v = pos / axis.relsizes[-1]
1641 x, y = axis._vtickpoint(axis, v)
1642 dx, dy = axis.vtickdirection(axis, v)
1643 x1 = x - dx * axis.innerticklength
1644 y1 = y - dy * axis.innerticklength
1645 x2 = x + dx * axis.outerticklength
1646 y2 = y + dy * axis.outerticklength
1647 graph.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(self.tickattrs))
1648 if self.baselineattrs is not None:
1649 if axis.vbaseline is not None: # XXX: subbaselines (as for splitlines)
1650 graph.stroke(axis.vbaseline(axis), *helper.ensuresequence(self.baselineattrs))
1651 for namebox in axis.nameboxes:
1652 graph.insert(namebox)
1653 axistitlepainter.paint(self, graph, axis)
1657 ################################################################################
1658 # axes
1659 ################################################################################
1661 class PartitionError(Exception): pass
1663 class _axis:
1665 def __init__(self, min=None, max=None, reverse=0, divisor=1,
1666 datavmin=None, datavmax=None, tickvmin=0, tickvmax=1,
1667 title=None, suffix=None, painter=axispainter(), dense=None):
1668 if None not in (min, max) and min > max:
1669 min, max, reverse = max, min, not reverse
1670 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
1672 self.datamin = self.datamax = self.tickmin = self.tickmax = None
1673 if datavmin is None:
1674 if self.fixmin:
1675 self.datavmin = 0
1676 else:
1677 self.datavmin = 0.05
1678 else:
1679 self.datavmin = datavmin
1680 if datavmax is None:
1681 if self.fixmax:
1682 self.datavmax = 1
1683 else:
1684 self.datavmax = 0.95
1685 else:
1686 self.datavmax = datavmax
1687 self.tickvmin = tickvmin
1688 self.tickvmax = tickvmax
1690 self.divisor = divisor
1691 self.title = title
1692 self.suffix = suffix
1693 self.painter = painter
1694 self.dense = dense
1695 self.canconvert = 0
1696 self.__setinternalrange()
1698 def __setinternalrange(self, min=None, max=None):
1699 if not self.fixmin and min is not None and (self.min is None or min < self.min):
1700 self.min = min
1701 if not self.fixmax and max is not None and (self.max is None or max > self.max):
1702 self.max = max
1703 if None not in (self.min, self.max):
1704 min, max, vmin, vmax = self.min, self.max, 0, 1
1705 self.canconvert = 1
1706 self.setbasepoints(((min, vmin), (max, vmax)))
1707 if not self.fixmin:
1708 if self.datamin is not None and self.convert(self.datamin) < self.datavmin:
1709 min, vmin = self.datamin, self.datavmin
1710 self.setbasepoints(((min, vmin), (max, vmax)))
1711 if self.tickmin is not None and self.convert(self.tickmin) < self.tickvmin:
1712 min, vmin = self.tickmin, self.tickvmin
1713 self.setbasepoints(((min, vmin), (max, vmax)))
1714 if not self.fixmax:
1715 if self.datamax is not None and self.convert(self.datamax) > self.datavmax:
1716 max, vmax = self.datamax, self.datavmax
1717 self.setbasepoints(((min, vmin), (max, vmax)))
1718 if self.tickmax is not None and self.convert(self.tickmax) > self.tickvmax:
1719 max, vmax = self.tickmax, self.tickvmax
1720 self.setbasepoints(((min, vmin), (max, vmax)))
1721 if self.reverse:
1722 self.setbasepoints(((min, vmax), (max, vmin)))
1724 def __getinternalrange(self):
1725 return self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax
1727 def __forceinternalrange(self, range):
1728 self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax = range
1729 self.__setinternalrange()
1731 def setdatarange(self, min, max):
1732 self.datamin, self.datamax = min, max
1733 self.__setinternalrange(min, max)
1735 def settickrange(self, min, max):
1736 self.tickmin, self.tickmax = min, max
1737 self.__setinternalrange(min, max)
1739 def getdatarange(self):
1740 if self.canconvert:
1741 if self.reverse:
1742 return self.invert(1-self.datavmin), self.invert(1-self.datavmax)
1743 else:
1744 return self.invert(self.datavmin), self.invert(self.datavmax)
1746 def gettickrange(self):
1747 if self.canconvert:
1748 if self.reverse:
1749 return self.invert(1-self.tickvmin), self.invert(1-self.tickvmax)
1750 else:
1751 return self.invert(self.tickvmin), self.invert(self.tickvmax)
1753 def dolayout(self, graph):
1754 if self.dense is not None:
1755 dense = self.dense
1756 else:
1757 dense = graph.dense
1758 min, max = self.gettickrange()
1759 if self.part is not None:
1760 self.ticks = self.part.defaultpart(min/self.divisor,
1761 max/self.divisor,
1762 not self.fixmin,
1763 not self.fixmax)
1764 else:
1765 self.ticks = []
1766 # lesspart and morepart can be called after defaultpart,
1767 # although some axes may share their autoparting ---
1768 # it works, because the axes are processed sequentially
1769 first = 1
1770 maxworse = 2
1771 worse = 0
1772 while worse < maxworse:
1773 if self.part is not None:
1774 newticks = self.part.lesspart()
1775 else:
1776 newticks = None
1777 if newticks is not None:
1778 if first:
1779 bestrate = self.rate.ratepart(self, self.ticks, dense)
1780 variants = [[bestrate, self.ticks]]
1781 first = 0
1782 newrate = self.rate.ratepart(self, newticks, dense)
1783 variants.append([newrate, newticks])
1784 if newrate < bestrate:
1785 bestrate = newrate
1786 worse = 0
1787 else:
1788 worse += 1
1789 else:
1790 worse += 1
1791 worse = 0
1792 while worse < maxworse:
1793 if self.part is not None:
1794 newticks = self.part.morepart()
1795 else:
1796 newticks = None
1797 if newticks is not None:
1798 if first:
1799 bestrate = self.rate.ratepart(self, self.ticks, dense)
1800 variants = [[bestrate, self.ticks]]
1801 first = 0
1802 newrate = self.rate.ratepart(self, newticks, dense)
1803 variants.append([newrate, newticks])
1804 if newrate < bestrate:
1805 bestrate = newrate
1806 worse = 0
1807 else:
1808 worse += 1
1809 else:
1810 worse += 1
1811 if not first:
1812 variants.sort()
1813 if self.painter is not None:
1814 i = 0
1815 bestrate = None
1816 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
1817 saverange = self.__getinternalrange()
1818 self.ticks = variants[i][1]
1819 if len(self.ticks):
1820 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
1821 self.layoutdata = layoutdata()
1822 self.painter.dolayout(graph, self)
1823 ratelayout = self.painter.ratelayout(graph, self, dense)
1824 if ratelayout is not None:
1825 variants[i][0] += ratelayout
1826 variants[i].append(self.layoutdata)
1827 else:
1828 variants[i][0] = None
1829 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
1830 bestrate = variants[i][0]
1831 self.__forceinternalrange(saverange)
1832 i += 1
1833 if bestrate is None:
1834 raise PartitionError("no valid axis partitioning found")
1835 variants = [variant for variant in variants[:i] if variant[0] is not None]
1836 variants.sort()
1837 self.ticks = variants[0][1]
1838 self.layoutdata = variants[0][2]
1839 else:
1840 for tick in self.ticks:
1841 tick.textbox = None
1842 self.layoutdata = layoutdata()
1843 self.layoutdata._extent = 0
1844 if len(self.ticks):
1845 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
1846 else:
1847 if len(self.ticks):
1848 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
1849 self.layoutdata = layoutdata()
1850 self.painter.dolayout(graph, self)
1851 graph.mindbbox(*[tick.textbox.bbox() for tick in self.ticks if tick.textbox is not None])
1853 def dopaint(self, graph):
1854 if self.painter is not None:
1855 self.painter.paint(graph, self)
1857 def createlinkaxis(self, **args):
1858 return linkaxis(self, **args)
1861 class linaxis(_axis, _linmap):
1863 def __init__(self, part=autolinpart(), rate=axisrater(), **args):
1864 _axis.__init__(self, **args)
1865 if self.fixmin and self.fixmax:
1866 self.relsize = self.max - self.min
1867 self.part = part
1868 self.rate = rate
1871 class logaxis(_axis, _logmap):
1873 def __init__(self, part=autologpart(), rate=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
1874 _axis.__init__(self, **args)
1875 if self.fixmin and self.fixmax:
1876 self.relsize = math.log(self.max) - math.log(self.min)
1877 self.part = part
1878 self.rate = rate
1881 class linkaxis:
1883 def __init__(self, linkedaxis, title=None, skipticklevel=None, skiplabellevel=0, painter=axispainter(zerolineattrs=None)):
1884 self.linkedaxis = linkedaxis
1885 while isinstance(self.linkedaxis, linkaxis):
1886 self.linkedaxis = self.linkedaxis.linkedaxis
1887 self.fixmin = self.linkedaxis.fixmin
1888 self.fixmax = self.linkedaxis.fixmax
1889 if self.fixmin:
1890 self.min = self.linkedaxis.min
1891 if self.fixmax:
1892 self.max = self.linkedaxis.max
1893 self.skipticklevel = skipticklevel
1894 self.skiplabellevel = skiplabellevel
1895 self.title = title
1896 self.painter = painter
1898 def ticks(self, ticks):
1899 result = []
1900 for _tick in ticks:
1901 ticklevel = _tick.ticklevel
1902 labellevel = _tick.labellevel
1903 if self.skipticklevel is not None and ticklevel >= self.skipticklevel:
1904 ticklevel = None
1905 if self.skiplabellevel is not None and labellevel >= self.skiplabellevel:
1906 labellevel = None
1907 if ticklevel is not None or labellevel is not None:
1908 result.append(tick(_tick.enum, _tick.denom, ticklevel, labellevel))
1909 return result
1910 # XXX: don't forget to calculate new label positions as soon as this is moved
1911 # outside of the paint method (when rating is moved into the axispainter)
1913 def getdatarange(self):
1914 return self.linkedaxis.getdatarange()
1916 def setdatarange(self, min, max):
1917 prevrange = self.linkedaxis.getdatarange()
1918 self.linkedaxis.setdatarange(min, max)
1919 if hasattr(self.linkedaxis, "ticks") and prevrange != self.linkedaxis.getdatarange():
1920 raise RuntimeError("linkaxis datarange setting performed while linked axis layout already fixed")
1922 def dolayout(self, graph):
1923 self.ticks = self.ticks(self.linkedaxis.ticks)
1924 self.convert = self.linkedaxis.convert
1925 self.divisor = self.linkedaxis.divisor
1926 self.suffix = self.linkedaxis.suffix
1927 self.layoutdata = layoutdata()
1928 self.painter.dolayout(graph, self)
1930 def dopaint(self, graph):
1931 self.painter.paint(graph, self)
1933 def createlinkaxis(self, **args):
1934 return linkaxis(self.linkedaxis)
1937 class splitaxis:
1939 def __init__(self, axislist, splitlist=0.5, splitdist=0.1, relsizesplitdist=1, title=None, painter=splitaxispainter()):
1940 self.title = title
1941 self.axislist = axislist
1942 self.painter = painter
1943 self.splitlist = list(helper.ensuresequence(splitlist))
1944 self.splitlist.sort()
1945 if len(self.axislist) != len(self.splitlist) + 1:
1946 for subaxis in self.axislist:
1947 if not isinstance(subaxis, linkaxis):
1948 raise ValueError("axislist and splitlist lengths do not fit together")
1949 for subaxis in self.axislist:
1950 if isinstance(subaxis, linkaxis):
1951 subaxis.vmin = subaxis.linkedaxis.vmin
1952 subaxis.vminover = subaxis.linkedaxis.vminover
1953 subaxis.vmax = subaxis.linkedaxis.vmax
1954 subaxis.vmaxover = subaxis.linkedaxis.vmaxover
1955 else:
1956 subaxis.vmin = None
1957 subaxis.vmax = None
1958 self.axislist[0].vmin = 0
1959 self.axislist[0].vminover = None
1960 self.axislist[-1].vmax = 1
1961 self.axislist[-1].vmaxover = None
1962 for i in xrange(len(self.splitlist)):
1963 if self.splitlist[i] is not None:
1964 self.axislist[i].vmax = self.splitlist[i] - 0.5*splitdist
1965 self.axislist[i].vmaxover = self.splitlist[i]
1966 self.axislist[i+1].vmin = self.splitlist[i] + 0.5*splitdist
1967 self.axislist[i+1].vminover = self.splitlist[i]
1968 i = 0
1969 while i < len(self.axislist):
1970 if self.axislist[i].vmax is None:
1971 j = relsize = relsize2 = 0
1972 while self.axislist[i + j].vmax is None:
1973 relsize += self.axislist[i + j].relsize + relsizesplitdist
1974 j += 1
1975 relsize += self.axislist[i + j].relsize
1976 vleft = self.axislist[i].vmin
1977 vright = self.axislist[i + j].vmax
1978 for k in range(i, i + j):
1979 relsize2 += self.axislist[k].relsize
1980 self.axislist[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
1981 relsize2 += 0.5 * relsizesplitdist
1982 self.axislist[k].vmaxover = self.axislist[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
1983 relsize2 += 0.5 * relsizesplitdist
1984 self.axislist[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
1985 if i == 0 and i + j + 1 == len(self.axislist):
1986 self.relsize = relsize
1987 i += j + 1
1988 else:
1989 i += 1
1991 self.fixmin = self.axislist[0].fixmin
1992 if self.fixmin:
1993 self.min = self.axislist[0].min
1994 self.fixmax = self.axislist[-1].fixmax
1995 if self.fixmax:
1996 self.max = self.axislist[-1].max
1997 self.divisor = 1
1998 self.suffix = ""
2000 def getdatarange(self):
2001 min = self.axislist[0].getdatarange()
2002 max = self.axislist[-1].getdatarange()
2003 try:
2004 return min[0], max[1]
2005 except TypeError:
2006 return None
2008 def setdatarange(self, min, max):
2009 self.axislist[0].setdatarange(min, None)
2010 self.axislist[-1].setdatarange(None, max)
2012 def gettickrange(self):
2013 min = self.axislist[0].gettickrange()
2014 max = self.axislist[-1].gettickrange()
2015 try:
2016 return min[0], max[1]
2017 except TypeError:
2018 return None
2020 def settickrange(self, min, max):
2021 self.axislist[0].settickrange(min, None)
2022 self.axislist[-1].settickrange(None, max)
2024 def convert(self, value):
2025 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2026 if value < self.axislist[0].max:
2027 return self.axislist[0].vmin + self.axislist[0].convert(value)*(self.axislist[0].vmax-self.axislist[0].vmin)
2028 for axis in self.axislist[1:-1]:
2029 if value > axis.min and value < axis.max:
2030 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
2031 if value > self.axislist[-1].min:
2032 return self.axislist[-1].vmin + self.axislist[-1].convert(value)*(self.axislist[-1].vmax-self.axislist[-1].vmin)
2033 raise ValueError("value couldn't be assigned to a split region")
2035 def dolayout(self, graph):
2036 self.layoutdata = layoutdata()
2037 self.painter.dolayout(graph, self)
2039 def dopaint(self, graph):
2040 self.painter.paint(graph, self)
2042 def createlinkaxis(self, painter=None, *args):
2043 if not len(args):
2044 return splitaxis([x.createlinkaxis() for x in self.axislist], splitlist=None)
2045 if len(args) != len(self.axislist):
2046 raise IndexError("length of the argument list doesn't fit to split number")
2047 if painter is None:
2048 painter = self.painter
2049 return splitaxis([x.createlinkaxis(**arg) for x, arg in zip(self.axislist, args)], painter=painter)
2052 class baraxis:
2054 def __init__(self, subaxis=None, multisubaxis=0, title=None, dist=0.5, firstdist=None, lastdist=None, names=None, texts={}, painter=baraxispainter()):
2055 self.dist = dist
2056 if firstdist is not None:
2057 self.firstdist = firstdist
2058 else:
2059 self.firstdist = 0.5 * dist
2060 if lastdist is not None:
2061 self.lastdist = lastdist
2062 else:
2063 self.lastdist = 0.5 * dist
2064 self.relsizes = None
2065 self.fixnames = 0
2066 self.names = []
2067 for name in helper.ensuresequence(names):
2068 self.setname(name)
2069 self.fixnames = names is not None
2070 self.multisubaxis = multisubaxis
2071 if self.multisubaxis:
2072 self.createsubaxis = subaxis
2073 self.subaxis = [self.createsubaxis.createsubaxis() for name in self.names]
2074 else:
2075 self.subaxis = subaxis
2076 self.title = title
2077 self.fixnames = 0
2078 self.texts = texts
2079 self.painter = painter
2081 def getdatarange(self):
2082 return None
2084 def setname(self, name, *subnames):
2085 # setting self.relsizes to None forces later recalculation
2086 if not self.fixnames:
2087 if name not in self.names:
2088 self.relsizes = None
2089 self.names.append(name)
2090 if self.multisubaxis:
2091 self.subaxis.append(self.createsubaxis.createsubaxis())
2092 if (not self.fixnames or name in self.names) and len(subnames):
2093 if self.multisubaxis:
2094 if self.subaxis[self.names.index(name)].setname(*subnames):
2095 self.relsizes = None
2096 else:
2097 #print self.subaxis, self.multisubaxis, subnames, name
2098 if self.subaxis.setname(*subnames):
2099 self.relsizes = None
2100 return self.relsizes is not None
2102 def updaterelsizes(self):
2103 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
2104 self.relsizes[-1] += self.lastdist - self.dist
2105 if self.multisubaxis:
2106 subrelsize = 0
2107 for i in range(1, len(self.relsizes)):
2108 self.subaxis[i-1].updaterelsizes()
2109 subrelsize += self.subaxis[i-1].relsizes[-1]
2110 self.relsizes[i] += subrelsize
2111 else:
2112 if self.subaxis is None:
2113 subrelsize = 1
2114 else:
2115 self.subaxis.updaterelsizes()
2116 subrelsize = self.subaxis.relsizes[-1]
2117 for i in range(1, len(self.relsizes)):
2118 self.relsizes[i] += i * subrelsize
2120 def convert(self, value):
2121 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2122 if not self.relsizes:
2123 self.updaterelsizes()
2124 pos = self.names.index(value[0])
2125 if len(value) == 2:
2126 if self.subaxis is None:
2127 subvalue = value[1]
2128 else:
2129 if self.multisubaxis:
2130 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
2131 else:
2132 subvalue = value[1] * self.subaxis.relsizes[-1]
2133 else:
2134 if self.multisubaxis:
2135 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
2136 else:
2137 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
2138 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
2140 def dolayout(self, graph):
2141 self.layoutdata = layoutdata()
2142 self.painter.dolayout(graph, self)
2144 def dopaint(self, graph):
2145 self.painter.paint(graph, self)
2147 def createlinkaxis(self, **args):
2148 if self.subaxis is not None:
2149 if self.multisubaxis:
2150 subaxis = [subaxis.createlinkaxis() for subaxis in self.subaxis]
2151 else:
2152 subaxis = self.subaxis.createlinkaxis()
2153 else:
2154 subaxis = None
2155 return baraxis(subaxis=subaxis, dist=self.dist, firstdist=self.firstdist, lastdist=self.lastdist, **args)
2157 createsubaxis = createlinkaxis
2160 ################################################################################
2161 # graph key
2162 ################################################################################
2165 # g = graph.graphxy(key=graph.key())
2166 # g.addkey(graph.key(), ...)
2169 class key:
2171 def __init__(self, dist="0.2 cm", pos = "tr", hinside = 1, vinside = 1, hdist="0.6 cm", vdist="0.4 cm",
2172 symbolwidth="0.5 cm", symbolheight="0.25 cm", symbolspace="0.2 cm",
2173 textattrs=textmodule.vshift.mathaxis):
2174 self.dist_str = dist
2175 self.pos = pos
2176 self.hinside = hinside
2177 self.vinside = vinside
2178 self.hdist_str = hdist
2179 self.vdist_str = vdist
2180 self.symbolwidth_str = symbolwidth
2181 self.symbolheight_str = symbolheight
2182 self.symbolspace_str = symbolspace
2183 self.textattrs = textattrs
2184 self.plotinfos = None
2185 if self.pos in ("tr", "rt"):
2186 self.right = 1
2187 self.top = 1
2188 elif self.pos in ("br", "rb"):
2189 self.right = 1
2190 self.top = 0
2191 elif self.pos in ("tl", "lt"):
2192 self.right = 0
2193 self.top = 1
2194 elif self.pos in ("bl", "lb"):
2195 self.right = 0
2196 self.top = 0
2197 else:
2198 raise RuntimeError("invalid pos attribute")
2200 def setplotinfos(self, *plotinfos):
2201 """set the plotinfos to be used in the key
2202 - call it exactly once"""
2203 if self.plotinfos is not None:
2204 raise RuntimeError("setplotinfo is called multiple times")
2205 self.plotinfos = plotinfos
2207 def dolayout(self, graph):
2208 """creates the layout of the key"""
2209 self._dist = unit.topt(unit.length(self.dist_str, default_type="v"))
2210 self._hdist = unit.topt(unit.length(self.hdist_str, default_type="v"))
2211 self._vdist = unit.topt(unit.length(self.vdist_str, default_type="v"))
2212 self._symbolwidth = unit.topt(unit.length(self.symbolwidth_str, default_type="v"))
2213 self._symbolheight = unit.topt(unit.length(self.symbolheight_str, default_type="v"))
2214 self._symbolspace = unit.topt(unit.length(self.symbolspace_str, default_type="v"))
2215 self.titles = []
2216 for plotinfo in self.plotinfos:
2217 self.titles.append(graph.texrunner._text(0, 0, plotinfo.data.title, *helper.ensuresequence(self.textattrs)))
2218 box._tile(self.titles, self._dist, 0, -1)
2219 box._linealignequal(self.titles, self._symbolwidth + self._symbolspace, 1, 0)
2221 def bbox(self):
2222 """return a bbox for the key
2223 method should be called after dolayout"""
2224 result = self.titles[0].bbox()
2225 for title in self.titles[1:]:
2226 result = result + title.bbox() + bbox._bbox(0, title.center[1] - 0.5 * self._symbolheight,
2227 0, title.center[1] + 0.5 * self._symbolheight)
2228 return result
2230 def paint(self, c, x, y):
2231 """paint the graph key into a canvas c at the position x and y (in postscript points)
2232 - method should be called after dolayout
2233 - the x, y alignment might be calculated by the graph using:
2234 - the bbox of the key as returned by the keys bbox method
2235 - the attributes _hdist, _vdist, hinside, and vinside of the key
2236 - the dimension and geometry of the graph"""
2237 sc = c.insert(canvas.canvas(trafo._translate(x, y)))
2238 for plotinfo, title in zip(self.plotinfos, self.titles):
2239 plotinfo.style.key(sc, 0, -0.5 * self._symbolheight + title.center[1],
2240 self._symbolwidth, self._symbolheight)
2241 sc.insert(title)
2244 ################################################################################
2245 # graph
2246 ################################################################################
2249 class plotinfo:
2251 def __init__(self, data, style):
2252 self.data = data
2253 self.style = style
2256 class graphxy(canvas.canvas):
2258 Names = "x", "y"
2260 def clipcanvas(self):
2261 return self.insert(canvas.canvas(canvas.clip(path._rect(self._xpos, self._ypos, self._width, self._height))))
2263 def plot(self, data, style=None):
2264 if self.haslayout:
2265 raise RuntimeError("layout setup was already performed")
2266 if style is None:
2267 if helper.issequence(data):
2268 raise RuntimeError("list plot needs an explicit style")
2269 if self.defaultstyle.has_key(data.defaultstyle):
2270 style = self.defaultstyle[data.defaultstyle].iterate()
2271 else:
2272 style = data.defaultstyle()
2273 self.defaultstyle[data.defaultstyle] = style
2274 plotinfos = []
2275 first = 1
2276 for d in helper.ensuresequence(data):
2277 if not first:
2278 style = style.iterate()
2279 first = 0
2280 if d is not None:
2281 d.setstyle(self, style)
2282 plotinfos.append(plotinfo(d, style))
2283 self.plotinfos.extend(plotinfos)
2284 if helper.issequence(data):
2285 return plotinfos
2286 return plotinfos[0]
2288 def addkey(self, key, *plotinfos):
2289 if self.haslayout:
2290 raise RuntimeError("layout setup was already performed")
2291 self.addkeys.append((key, plotinfos))
2293 def _vxtickpoint(self, axis, v):
2294 return (self._xpos+v*self._width, axis.axispos)
2296 def _vytickpoint(self, axis, v):
2297 return (axis.axispos, self._ypos+v*self._height)
2299 def vtickdirection(self, axis, v):
2300 return axis.fixtickdirection
2302 def _pos(self, x, y, xaxis=None, yaxis=None):
2303 if xaxis is None: xaxis = self.axes["x"]
2304 if yaxis is None: yaxis = self.axes["y"]
2305 return self._xpos+xaxis.convert(x)*self._width, self._ypos+yaxis.convert(y)*self._height
2307 def pos(self, x, y, xaxis=None, yaxis=None):
2308 if xaxis is None: xaxis = self.axes["x"]
2309 if yaxis is None: yaxis = self.axes["y"]
2310 return self.xpos+xaxis.convert(x)*self.width, self.ypos+yaxis.convert(y)*self.height
2312 def _vpos(self, vx, vy):
2313 return self._xpos+vx*self._width, self._ypos+vy*self._height
2315 def vpos(self, vx, vy):
2316 return self.xpos+vx*self.width, self.ypos+vy*self.height
2318 def xbaseline(self, axis, x1, x2, shift=0, xaxis=None):
2319 if xaxis is None: xaxis = self.axes["x"]
2320 v1, v2 = xaxis.convert(x1), xaxis.convert(x2)
2321 return path._line(self._xpos+v1*self._width, axis.axispos+shift,
2322 self._xpos+v2*self._width, axis.axispos+shift)
2324 def ybaseline(self, axis, y1, y2, shift=0, yaxis=None):
2325 if yaxis is None: yaxis = self.axes["y"]
2326 v1, v2 = yaxis.convert(y1), yaxis.convert(y2)
2327 return path._line(axis.axispos+shift, self._ypos+v1*self._height,
2328 axis.axispos+shift, self._ypos+v2*self._height)
2330 def vxbaseline(self, axis, v1=None, v2=None, shift=0):
2331 if v1 is None: v1 = 0
2332 if v2 is None: v2 = 1
2333 return path._line(self._xpos+v1*self._width, axis.axispos+shift,
2334 self._xpos+v2*self._width, axis.axispos+shift)
2336 def vybaseline(self, axis, v1=None, v2=None, shift=0):
2337 if v1 is None: v1 = 0
2338 if v2 is None: v2 = 1
2339 return path._line(axis.axispos+shift, self._ypos+v1*self._height,
2340 axis.axispos+shift, self._ypos+v2*self._height)
2342 def xgridpath(self, x, xaxis=None):
2343 if xaxis is None: xaxis = self.axes["x"]
2344 v = xaxis.convert(x)
2345 return path._line(self._xpos+v*self._width, self._ypos,
2346 self._xpos+v*self._width, self._ypos+self._height)
2348 def ygridpath(self, y, yaxis=None):
2349 if yaxis is None: yaxis = self.axes["y"]
2350 v = yaxis.convert(y)
2351 return path._line(self._xpos, self._ypos+v*self._height,
2352 self._xpos+self._width, self._ypos+v*self._height)
2354 def vxgridpath(self, v):
2355 return path._line(self._xpos+v*self._width, self._ypos,
2356 self._xpos+v*self._width, self._ypos+self._height)
2358 def vygridpath(self, v):
2359 return path._line(self._xpos, self._ypos+v*self._height,
2360 self._xpos+self._width, self._ypos+v*self._height)
2362 def _addpos(self, x, y, dx, dy):
2363 return x+dx, y+dy
2365 def _connect(self, x1, y1, x2, y2):
2366 return path._lineto(x2, y2)
2368 def keynum(self, key):
2369 try:
2370 while key[0] in string.letters:
2371 key = key[1:]
2372 return int(key)
2373 except IndexError:
2374 return 1
2376 def gatherranges(self):
2377 ranges = {}
2378 for plotinfo in self.plotinfos:
2379 pdranges = plotinfo.data.getranges()
2380 if pdranges is not None:
2381 for key in pdranges.keys():
2382 if key not in ranges.keys():
2383 ranges[key] = pdranges[key]
2384 else:
2385 ranges[key] = (min(ranges[key][0], pdranges[key][0]),
2386 max(ranges[key][1], pdranges[key][1]))
2387 # known ranges are also set as ranges for the axes
2388 for key, axis in self.axes.items():
2389 if key in ranges.keys():
2390 axis.setdatarange(*ranges[key])
2391 ranges[key] = axis.getdatarange()
2392 if ranges[key] is None:
2393 del ranges[key]
2394 return ranges
2396 def removedomethod(self, method):
2397 hadmethod = 0
2398 while 1:
2399 try:
2400 self.domethods.remove(method)
2401 hadmethod = 1
2402 except ValueError:
2403 return hadmethod
2405 def dolayout(self):
2406 if not self.removedomethod(self.dolayout): return
2407 self.haslayout = 1
2408 # create list of ranges
2409 # 1. gather ranges
2410 ranges = self.gatherranges()
2411 # 2. calculate additional ranges out of known ranges
2412 for plotinfo in self.plotinfos:
2413 plotinfo.data.setranges(ranges)
2414 # 3. gather ranges again
2415 self.gatherranges()
2417 # do the layout for all axes
2418 axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
2419 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
2420 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
2421 self._xaxisextents = [0, 0]
2422 self._yaxisextents = [0, 0]
2423 needxaxisdist = [0, 0]
2424 needyaxisdist = [0, 0]
2425 items = list(self.axes.items())
2426 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
2427 for key, axis in items:
2428 num = self.keynum(key)
2429 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
2430 num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
2431 if XPattern.match(key):
2432 if needxaxisdist[num2]:
2433 self._xaxisextents[num2] += axesdist
2434 axis.axispos = self._ypos+num2*self._height + num3*self._xaxisextents[num2]
2435 axis._vtickpoint = self._vxtickpoint
2436 axis.fixtickdirection = (0, num3)
2437 axis.vgridpath = self.vxgridpath
2438 axis.vbaseline = self.vxbaseline
2439 axis.gridpath = self.xgridpath
2440 axis.baseline = self.xbaseline
2441 elif YPattern.match(key):
2442 if needyaxisdist[num2]:
2443 self._yaxisextents[num2] += axesdist
2444 axis.axispos = self._xpos+num2*self._width + num3*self._yaxisextents[num2]
2445 axis._vtickpoint = self._vytickpoint
2446 axis.fixtickdirection = (num3, 0)
2447 axis.vgridpath = self.vygridpath
2448 axis.vbaseline = self.vybaseline
2449 axis.gridpath = self.ygridpath
2450 axis.baseline = self.ybaseline
2451 else:
2452 raise ValueError("Axis key '%s' not allowed" % key)
2453 axis.vtickdirection = self.vtickdirection
2454 axis.dolayout(self)
2455 if XPattern.match(key):
2456 self._xaxisextents[num2] += axis.layoutdata._extent
2457 needxaxisdist[num2] = 1
2458 if YPattern.match(key):
2459 self._yaxisextents[num2] += axis.layoutdata._extent
2460 needyaxisdist[num2] = 1
2462 def dobackground(self):
2463 self.dolayout()
2464 if not self.removedomethod(self.dobackground): return
2465 if self.backgroundattrs is not None:
2466 self.draw(path._rect(self._xpos, self._ypos, self._width, self._height),
2467 *helper.ensuresequence(self.backgroundattrs))
2469 def doaxes(self):
2470 self.dolayout()
2471 if not self.removedomethod(self.doaxes): return
2472 for axis in self.axes.values():
2473 axis.dopaint(self)
2475 def dodata(self):
2476 self.dolayout()
2477 if not self.removedomethod(self.dodata): return
2478 for plotinfo in self.plotinfos:
2479 plotinfo.data.draw(self)
2481 def _dokey(self, key, *plotinfos):
2482 key.setplotinfos(*plotinfos)
2483 key.dolayout(self)
2484 bbox = key.bbox()
2485 if key.right:
2486 if key.hinside:
2487 x = self._xpos + self._width - bbox.urx - key._hdist
2488 else:
2489 x = self._xpos + self._width - bbox.llx + key._hdist
2490 else:
2491 if key.hinside:
2492 x = self._xpos - bbox.llx + key._hdist
2493 else:
2494 x = self._xpos - bbox.urx - key._hdist
2495 if key.top:
2496 if key.vinside:
2497 y = self._ypos + self._height - bbox.ury - key._vdist
2498 else:
2499 y = self._ypos + self._height - bbox.lly + key._vdist
2500 else:
2501 if key.vinside:
2502 y = self._ypos - bbox.lly + key._vdist
2503 else:
2504 y = self._ypos - bbox.ury - key._vdist
2505 self.mindbbox(bbox.transformed(trafo._translate(x, y)))
2506 key.paint(self, x, y)
2508 def dokey(self):
2509 self.dolayout()
2510 if not self.removedomethod(self.dokey): return
2511 if self.key is not None:
2512 self._dokey(self.key, *self.plotinfos)
2513 for key, plotinfos in self.addkeys:
2514 self._dokey(key, *plotinfos)
2516 def finish(self):
2517 while len(self.domethods):
2518 self.domethods[0]()
2520 def initwidthheight(self, width, height, ratio):
2521 if (width is not None) and (height is None):
2522 self.width = unit.length(width)
2523 self.height = (1.0/ratio) * self.width
2524 elif (height is not None) and (width is None):
2525 self.height = unit.length(height)
2526 self.width = ratio * self.height
2527 else:
2528 self.width = unit.length(width)
2529 self.height = unit.length(height)
2530 self._width = unit.topt(self.width)
2531 self._height = unit.topt(self.height)
2532 if self._width <= 0: raise ValueError("width <= 0")
2533 if self._height <= 0: raise ValueError("height <= 0")
2535 def initaxes(self, axes, addlinkaxes=0):
2536 for key in self.Names:
2537 if not axes.has_key(key):
2538 axes[key] = linaxis()
2539 elif axes[key] is None:
2540 del axes[key]
2541 if addlinkaxes:
2542 if not axes.has_key(key + "2") and axes.has_key(key):
2543 axes[key + "2"] = axes[key].createlinkaxis()
2544 elif axes[key + "2"] is None:
2545 del axes[key + "2"]
2546 self.axes = axes
2548 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
2549 key=None, backgroundattrs=None, dense=1, axesdist="0.8 cm", **axes):
2550 canvas.canvas.__init__(self)
2551 self.xpos = unit.length(xpos)
2552 self.ypos = unit.length(ypos)
2553 self._xpos = unit.topt(self.xpos)
2554 self._ypos = unit.topt(self.ypos)
2555 self.initwidthheight(width, height, ratio)
2556 self.initaxes(axes, 1)
2557 self.key = key
2558 self.backgroundattrs = backgroundattrs
2559 self.dense = dense
2560 self.axesdist_str = axesdist
2561 self.plotinfos = []
2562 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata, self.dokey]
2563 self.haslayout = 0
2564 self.defaultstyle = {}
2565 self.addkeys = []
2566 self.mindbboxes = []
2568 def mindbbox(self, *boxes):
2569 self.mindbboxes.extend(boxes)
2571 def bbox(self):
2572 self.finish()
2573 result = bbox._bbox(self._xpos - self._yaxisextents[0],
2574 self._ypos - self._xaxisextents[0],
2575 self._xpos + self._width + self._yaxisextents[1],
2576 self._ypos + self._height + self._xaxisextents[1])
2577 for box in self.mindbboxes:
2578 result = result + box
2579 return result
2581 def write(self, file):
2582 self.finish()
2583 canvas.canvas.write(self, file)
2587 # some thoughts, but deferred right now
2589 # class graphxyz(graphxy):
2591 # Names = "x", "y", "z"
2593 # def _vxtickpoint(self, axis, v):
2594 # return self._vpos(v, axis.vypos, axis.vzpos)
2596 # def _vytickpoint(self, axis, v):
2597 # return self._vpos(axis.vxpos, v, axis.vzpos)
2599 # def _vztickpoint(self, axis, v):
2600 # return self._vpos(axis.vxpos, axis.vypos, v)
2602 # def vxtickdirection(self, axis, v):
2603 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
2604 # x2, y2 = self._vpos(v, 0.5, 0)
2605 # dx, dy = x1 - x2, y1 - y2
2606 # norm = math.sqrt(dx*dx + dy*dy)
2607 # return dx/norm, dy/norm
2609 # def vytickdirection(self, axis, v):
2610 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
2611 # x2, y2 = self._vpos(0.5, v, 0)
2612 # dx, dy = x1 - x2, y1 - y2
2613 # norm = math.sqrt(dx*dx + dy*dy)
2614 # return dx/norm, dy/norm
2616 # def vztickdirection(self, axis, v):
2617 # return -1, 0
2618 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
2619 # x2, y2 = self._vpos(0.5, 0.5, v)
2620 # dx, dy = x1 - x2, y1 - y2
2621 # norm = math.sqrt(dx*dx + dy*dy)
2622 # return dx/norm, dy/norm
2624 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
2625 # if xaxis is None: xaxis = self.axes["x"]
2626 # if yaxis is None: yaxis = self.axes["y"]
2627 # if zaxis is None: zaxis = self.axes["z"]
2628 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
2630 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
2631 # if xaxis is None: xaxis = self.axes["x"]
2632 # if yaxis is None: yaxis = self.axes["y"]
2633 # if zaxis is None: zaxis = self.axes["z"]
2634 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
2636 # def _vpos(self, vx, vy, vz):
2637 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
2638 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
2639 # + self.a[2]*self.b[0]*(y-self.eye[1])
2640 # + self.a[1]*self.b[2]*(x-self.eye[0])
2641 # - self.a[2]*self.b[1]*(x-self.eye[0])
2642 # - self.a[0]*self.b[2]*(y-self.eye[1])
2643 # - self.a[1]*self.b[0]*(z-self.eye[2]))
2644 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
2645 # + self.eye[2]*self.b[0]*(y-self.eye[1])
2646 # + self.eye[1]*self.b[2]*(x-self.eye[0])
2647 # - self.eye[2]*self.b[1]*(x-self.eye[0])
2648 # - self.eye[0]*self.b[2]*(y-self.eye[1])
2649 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
2650 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
2651 # + self.a[2]*self.eye[0]*(y-self.eye[1])
2652 # + self.a[1]*self.eye[2]*(x-self.eye[0])
2653 # - self.a[2]*self.eye[1]*(x-self.eye[0])
2654 # - self.a[0]*self.eye[2]*(y-self.eye[1])
2655 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
2656 # return da/d0 + self._xpos, db/d0 + self._ypos
2658 # def vpos(self, vx, vy, vz):
2659 # tx, ty = self._vpos(vx, vy, vz)
2660 # return unit.t_pt(tx), unit.t_pt(ty)
2662 # def xbaseline(self, axis, x1, x2, shift=0, xaxis=None):
2663 # if xaxis is None: xaxis = self.axes["x"]
2664 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2), shift)
2666 # def ybaseline(self, axis, y1, y2, shift=0, yaxis=None):
2667 # if yaxis is None: yaxis = self.axes["y"]
2668 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2), shift)
2670 # def zbaseline(self, axis, z1, z2, shift=0, zaxis=None):
2671 # if zaxis is None: zaxis = self.axes["z"]
2672 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2), shift)
2674 # def vxbaseline(self, axis, v1, v2, shift=0):
2675 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
2676 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
2677 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
2678 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
2680 # def vybaseline(self, axis, v1, v2, shift=0):
2681 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
2682 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
2683 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
2684 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
2686 # def vzbaseline(self, axis, v1, v2, shift=0):
2687 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
2688 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
2689 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
2690 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
2692 # def xgridpath(self, x, xaxis=None):
2693 # assert 0
2694 # if xaxis is None: xaxis = self.axes["x"]
2695 # v = xaxis.convert(x)
2696 # return path._line(self._xpos+v*self._width, self._ypos,
2697 # self._xpos+v*self._width, self._ypos+self._height)
2699 # def ygridpath(self, y, yaxis=None):
2700 # assert 0
2701 # if yaxis is None: yaxis = self.axes["y"]
2702 # v = yaxis.convert(y)
2703 # return path._line(self._xpos, self._ypos+v*self._height,
2704 # self._xpos+self._width, self._ypos+v*self._height)
2706 # def zgridpath(self, z, zaxis=None):
2707 # assert 0
2708 # if zaxis is None: zaxis = self.axes["z"]
2709 # v = zaxis.convert(z)
2710 # return path._line(self._xpos, self._zpos+v*self._height,
2711 # self._xpos+self._width, self._zpos+v*self._height)
2713 # def vxgridpath(self, v):
2714 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
2715 # path._lineto(*self._vpos(v, 0, 1)),
2716 # path._lineto(*self._vpos(v, 1, 1)),
2717 # path._lineto(*self._vpos(v, 1, 0)),
2718 # path.closepath())
2720 # def vygridpath(self, v):
2721 # return path.path(path._moveto(*self._vpos(0, v, 0)),
2722 # path._lineto(*self._vpos(0, v, 1)),
2723 # path._lineto(*self._vpos(1, v, 1)),
2724 # path._lineto(*self._vpos(1, v, 0)),
2725 # path.closepath())
2727 # def vzgridpath(self, v):
2728 # return path.path(path._moveto(*self._vpos(0, 0, v)),
2729 # path._lineto(*self._vpos(0, 1, v)),
2730 # path._lineto(*self._vpos(1, 1, v)),
2731 # path._lineto(*self._vpos(1, 0, v)),
2732 # path.closepath())
2734 # def _addpos(self, x, y, dx, dy):
2735 # assert 0
2736 # return x+dx, y+dy
2738 # def _connect(self, x1, y1, x2, y2):
2739 # assert 0
2740 # return path._lineto(x2, y2)
2742 # def doaxes(self):
2743 # self.dolayout()
2744 # if not self.removedomethod(self.doaxes): return
2745 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
2746 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
2747 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
2748 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[2])
2749 # items = list(self.axes.items())
2750 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
2751 # for key, axis in items:
2752 # num = self.keynum(key)
2753 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
2754 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
2755 # if XPattern.match(key):
2756 # axis.vypos = 0
2757 # axis.vzpos = 0
2758 # axis._vtickpoint = self._vxtickpoint
2759 # axis.vgridpath = self.vxgridpath
2760 # axis.vbaseline = self.vxbaseline
2761 # axis.vtickdirection = self.vxtickdirection
2762 # elif YPattern.match(key):
2763 # axis.vxpos = 0
2764 # axis.vzpos = 0
2765 # axis._vtickpoint = self._vytickpoint
2766 # axis.vgridpath = self.vygridpath
2767 # axis.vbaseline = self.vybaseline
2768 # axis.vtickdirection = self.vytickdirection
2769 # elif ZPattern.match(key):
2770 # axis.vxpos = 0
2771 # axis.vypos = 0
2772 # axis._vtickpoint = self._vztickpoint
2773 # axis.vgridpath = self.vzgridpath
2774 # axis.vbaseline = self.vzbaseline
2775 # axis.vtickdirection = self.vztickdirection
2776 # else:
2777 # raise ValueError("Axis key '%s' not allowed" % key)
2778 # if axis.painter is not None:
2779 # axis.dopaint(self)
2780 # # if XPattern.match(key):
2781 # # self._xaxisextents[num2] += axis._extent
2782 # # needxaxisdist[num2] = 1
2783 # # if YPattern.match(key):
2784 # # self._yaxisextents[num2] += axis._extent
2785 # # needyaxisdist[num2] = 1
2787 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
2788 # phi=30, theta=30, distance=1,
2789 # backgroundattrs=None, axesdist="0.8 cm", **axes):
2790 # canvas.canvas.__init__(self)
2791 # self.tex = tex
2792 # self.xpos = xpos
2793 # self.ypos = ypos
2794 # self._xpos = unit.topt(xpos)
2795 # self._ypos = unit.topt(ypos)
2796 # self._width = unit.topt(width)
2797 # self._height = unit.topt(height)
2798 # self._depth = unit.topt(depth)
2799 # self.width = width
2800 # self.height = height
2801 # self.depth = depth
2802 # if self._width <= 0: raise ValueError("width < 0")
2803 # if self._height <= 0: raise ValueError("height < 0")
2804 # if self._depth <= 0: raise ValueError("height < 0")
2805 # self._distance = distance*math.sqrt(self._width*self._width+
2806 # self._height*self._height+
2807 # self._depth*self._depth)
2808 # phi *= -math.pi/180
2809 # theta *= math.pi/180
2810 # self.a = (-math.sin(phi), math.cos(phi), 0)
2811 # self.b = (-math.cos(phi)*math.sin(theta),
2812 # -math.sin(phi)*math.sin(theta),
2813 # math.cos(theta))
2814 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
2815 # self._distance*math.sin(phi)*math.cos(theta),
2816 # self._distance*math.sin(theta))
2817 # self.initaxes(axes)
2818 # self.axesdist_str = axesdist
2819 # self.backgroundattrs = backgroundattrs
2821 # self.data = []
2822 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
2823 # self.haslayout = 0
2824 # self.defaultstyle = {}
2826 # def bbox(self):
2827 # self.finish()
2828 # return bbox._bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
2831 ################################################################################
2832 # attr changers
2833 ################################################################################
2836 #class _Ichangeattr:
2837 # """attribute changer
2838 # is an iterator for attributes where an attribute
2839 # is not refered by just a number (like for a sequence),
2840 # but also by the number of attributes requested
2841 # by calls of the next method (like for an color palette)
2842 # (you should ensure to call all needed next before the attr)
2844 # the attribute itself is implemented by overloading the _attr method"""
2846 # def attr(self):
2847 # "get an attribute"
2849 # def next(self):
2850 # "get an attribute changer for the next attribute"
2853 class _changeattr: pass
2856 class changeattr(_changeattr):
2858 def __init__(self):
2859 self.counter = 1
2861 def getattr(self):
2862 return self.attr(0)
2864 def iterate(self):
2865 newindex = self.counter
2866 self.counter += 1
2867 return refattr(self, newindex)
2870 class refattr(_changeattr):
2872 def __init__(self, ref, index):
2873 self.ref = ref
2874 self.index = index
2876 def getattr(self):
2877 return self.ref.attr(self.index)
2879 def iterate(self):
2880 return self.ref.iterate()
2883 # helper routines for a using attrs
2885 def _getattr(attr):
2886 """get attr out of a attr/changeattr"""
2887 if isinstance(attr, _changeattr):
2888 return attr.getattr()
2889 return attr
2892 def _getattrs(attrs):
2893 """get attrs out of a sequence of attr/changeattr"""
2894 if attrs is not None:
2895 result = []
2896 for attr in helper.ensuresequence(attrs):
2897 if isinstance(attr, _changeattr):
2898 attr = attr.getattr()
2899 if attr is not None:
2900 result.append(attr)
2901 if len(result) or not len(attrs):
2902 return result
2905 def _iterateattr(attr):
2906 """perform next to a attr/changeattr"""
2907 if isinstance(attr, _changeattr):
2908 return attr.iterate()
2909 return attr
2912 def _iterateattrs(attrs):
2913 """perform next to a sequence of attr/changeattr"""
2914 if attrs is not None:
2915 result = []
2916 for attr in helper.ensuresequence(attrs):
2917 if isinstance(attr, _changeattr):
2918 result.append(attr.iterate())
2919 else:
2920 result.append(attr)
2921 return result
2924 class changecolor(changeattr):
2926 def __init__(self, palette):
2927 changeattr.__init__(self)
2928 self.palette = palette
2930 def attr(self, index):
2931 if self.counter != 1:
2932 return self.palette.getcolor(index/float(self.counter-1))
2933 else:
2934 return self.palette.getcolor(0)
2937 class _changecolorgray(changecolor):
2939 def __init__(self, palette=color.palette.Gray):
2940 changecolor.__init__(self, palette)
2942 _changecolorgrey = _changecolorgray
2945 class _changecolorreversegray(changecolor):
2947 def __init__(self, palette=color.palette.ReverseGray):
2948 changecolor.__init__(self, palette)
2950 _changecolorreversegrey = _changecolorreversegray
2953 class _changecolorredblack(changecolor):
2955 def __init__(self, palette=color.palette.RedBlack):
2956 changecolor.__init__(self, palette)
2959 class _changecolorblackred(changecolor):
2961 def __init__(self, palette=color.palette.BlackRed):
2962 changecolor.__init__(self, palette)
2965 class _changecolorredwhite(changecolor):
2967 def __init__(self, palette=color.palette.RedWhite):
2968 changecolor.__init__(self, palette)
2971 class _changecolorwhitered(changecolor):
2973 def __init__(self, palette=color.palette.WhiteRed):
2974 changecolor.__init__(self, palette)
2977 class _changecolorgreenblack(changecolor):
2979 def __init__(self, palette=color.palette.GreenBlack):
2980 changecolor.__init__(self, palette)
2983 class _changecolorblackgreen(changecolor):
2985 def __init__(self, palette=color.palette.BlackGreen):
2986 changecolor.__init__(self, palette)
2989 class _changecolorgreenwhite(changecolor):
2991 def __init__(self, palette=color.palette.GreenWhite):
2992 changecolor.__init__(self, palette)
2995 class _changecolorwhitegreen(changecolor):
2997 def __init__(self, palette=color.palette.WhiteGreen):
2998 changecolor.__init__(self, palette)
3001 class _changecolorblueblack(changecolor):
3003 def __init__(self, palette=color.palette.BlueBlack):
3004 changecolor.__init__(self, palette)
3007 class _changecolorblackblue(changecolor):
3009 def __init__(self, palette=color.palette.BlackBlue):
3010 changecolor.__init__(self, palette)
3013 class _changecolorbluewhite(changecolor):
3015 def __init__(self, palette=color.palette.BlueWhite):
3016 changecolor.__init__(self, palette)
3019 class _changecolorwhiteblue(changecolor):
3021 def __init__(self, palette=color.palette.WhiteBlue):
3022 changecolor.__init__(self, palette)
3025 class _changecolorredgreen(changecolor):
3027 def __init__(self, palette=color.palette.RedGreen):
3028 changecolor.__init__(self, palette)
3031 class _changecolorredblue(changecolor):
3033 def __init__(self, palette=color.palette.RedBlue):
3034 changecolor.__init__(self, palette)
3037 class _changecolorgreenred(changecolor):
3039 def __init__(self, palette=color.palette.GreenRed):
3040 changecolor.__init__(self, palette)
3043 class _changecolorgreenblue(changecolor):
3045 def __init__(self, palette=color.palette.GreenBlue):
3046 changecolor.__init__(self, palette)
3049 class _changecolorbluered(changecolor):
3051 def __init__(self, palette=color.palette.BlueRed):
3052 changecolor.__init__(self, palette)
3055 class _changecolorbluegreen(changecolor):
3057 def __init__(self, palette=color.palette.BlueGreen):
3058 changecolor.__init__(self, palette)
3061 class _changecolorrainbow(changecolor):
3063 def __init__(self, palette=color.palette.Rainbow):
3064 changecolor.__init__(self, palette)
3067 class _changecolorreverserainbow(changecolor):
3069 def __init__(self, palette=color.palette.ReverseRainbow):
3070 changecolor.__init__(self, palette)
3073 class _changecolorhue(changecolor):
3075 def __init__(self, palette=color.palette.Hue):
3076 changecolor.__init__(self, palette)
3079 class _changecolorreversehue(changecolor):
3081 def __init__(self, palette=color.palette.ReverseHue):
3082 changecolor.__init__(self, palette)
3085 changecolor.Gray = _changecolorgray
3086 changecolor.Grey = _changecolorgrey
3087 changecolor.Reversegray = _changecolorreversegray
3088 changecolor.Reversegrey = _changecolorreversegrey
3089 changecolor.RedBlack = _changecolorredblack
3090 changecolor.BlackRed = _changecolorblackred
3091 changecolor.RedWhite = _changecolorredwhite
3092 changecolor.WhiteRed = _changecolorwhitered
3093 changecolor.GreenBlack = _changecolorgreenblack
3094 changecolor.BlackGreen = _changecolorblackgreen
3095 changecolor.GreenWhite = _changecolorgreenwhite
3096 changecolor.WhiteGreen = _changecolorwhitegreen
3097 changecolor.BlueBlack = _changecolorblueblack
3098 changecolor.BlackBlue = _changecolorblackblue
3099 changecolor.BlueWhite = _changecolorbluewhite
3100 changecolor.WhiteBlue = _changecolorwhiteblue
3101 changecolor.RedGreen = _changecolorredgreen
3102 changecolor.RedBlue = _changecolorredblue
3103 changecolor.GreenRed = _changecolorgreenred
3104 changecolor.GreenBlue = _changecolorgreenblue
3105 changecolor.BlueRed = _changecolorbluered
3106 changecolor.BlueGreen = _changecolorbluegreen
3107 changecolor.Rainbow = _changecolorrainbow
3108 changecolor.ReverseRainbow = _changecolorreverserainbow
3109 changecolor.Hue = _changecolorhue
3110 changecolor.ReverseHue = _changecolorreversehue
3113 class changesequence(changeattr):
3114 """cycles through a sequence"""
3116 def __init__(self, *sequence):
3117 changeattr.__init__(self)
3118 if not len(sequence):
3119 sequence = self.defaultsequence
3120 self.sequence = sequence
3122 def attr(self, index):
3123 return self.sequence[index % len(self.sequence)]
3126 class changelinestyle(changesequence):
3127 defaultsequence = (canvas.linestyle.solid,
3128 canvas.linestyle.dashed,
3129 canvas.linestyle.dotted,
3130 canvas.linestyle.dashdotted)
3133 class changestrokedfilled(changesequence):
3134 defaultsequence = (canvas.stroked(), canvas.filled())
3137 class changefilledstroked(changesequence):
3138 defaultsequence = (canvas.filled(), canvas.stroked())
3142 ################################################################################
3143 # styles
3144 ################################################################################
3147 class symbol:
3149 def cross(self, x, y):
3150 return (path._moveto(x-0.5*self._size, y-0.5*self._size),
3151 path._lineto(x+0.5*self._size, y+0.5*self._size),
3152 path._moveto(x-0.5*self._size, y+0.5*self._size),
3153 path._lineto(x+0.5*self._size, y-0.5*self._size))
3155 def plus(self, x, y):
3156 return (path._moveto(x-0.707106781*self._size, y),
3157 path._lineto(x+0.707106781*self._size, y),
3158 path._moveto(x, y-0.707106781*self._size),
3159 path._lineto(x, y+0.707106781*self._size))
3161 def square(self, x, y):
3162 return (path._moveto(x-0.5*self._size, y-0.5 * self._size),
3163 path._lineto(x+0.5*self._size, y-0.5 * self._size),
3164 path._lineto(x+0.5*self._size, y+0.5 * self._size),
3165 path._lineto(x-0.5*self._size, y+0.5 * self._size),
3166 path.closepath())
3168 def triangle(self, x, y):
3169 return (path._moveto(x-0.759835685*self._size, y-0.438691337*self._size),
3170 path._lineto(x+0.759835685*self._size, y-0.438691337*self._size),
3171 path._lineto(x, y+0.877382675*self._size),
3172 path.closepath())
3174 def circle(self, x, y):
3175 return (path._arc(x, y, 0.564189583*self._size, 0, 360),
3176 path.closepath())
3178 def diamond(self, x, y):
3179 return (path._moveto(x-0.537284965*self._size, y),
3180 path._lineto(x, y-0.930604859*self._size),
3181 path._lineto(x+0.537284965*self._size, y),
3182 path._lineto(x, y+0.930604859*self._size),
3183 path.closepath())
3185 def __init__(self, symbol=helper.nodefault,
3186 size="0.2 cm", symbolattrs=canvas.stroked(),
3187 errorscale=0.5, errorbarattrs=(),
3188 lineattrs=None):
3189 self.size_str = size
3190 if symbol is helper.nodefault:
3191 self._symbol = changesymbol.cross()
3192 else:
3193 self._symbol = symbol
3194 self._symbolattrs = symbolattrs
3195 self.errorscale = errorscale
3196 self._errorbarattrs = errorbarattrs
3197 self._lineattrs = lineattrs
3199 def iteratedict(self):
3200 result = {}
3201 result["symbol"] = _iterateattr(self._symbol)
3202 result["size"] = _iterateattr(self.size_str)
3203 result["symbolattrs"] = _iterateattrs(self._symbolattrs)
3204 result["errorscale"] = _iterateattr(self.errorscale)
3205 result["errorbarattrs"] = _iterateattrs(self._errorbarattrs)
3206 result["lineattrs"] = _iterateattrs(self._lineattrs)
3207 return result
3209 def iterate(self):
3210 return symbol(**self.iteratedict())
3212 def othercolumnkey(self, key, index):
3213 raise ValueError("unsuitable key '%s'" % key)
3215 def setcolumns(self, graph, columns):
3216 def checkpattern(key, index, pattern, iskey, isindex):
3217 if key is not None:
3218 match = pattern.match(key)
3219 if match:
3220 if isindex is not None: raise ValueError("multiple key specification")
3221 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
3222 key = None
3223 iskey = match.groups()[0]
3224 isindex = index
3225 return key, iskey, isindex
3227 self.xi = self.xmini = self.xmaxi = None
3228 self.dxi = self.dxmini = self.dxmaxi = None
3229 self.yi = self.ymini = self.ymaxi = None
3230 self.dyi = self.dymini = self.dymaxi = None
3231 self.xkey = self.ykey = None
3232 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
3233 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3234 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3235 XMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
3236 YMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
3237 XMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
3238 YMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
3239 DXPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3240 DYPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3241 DXMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
3242 DYMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
3243 DXMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
3244 DYMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
3245 for key, index in columns.items():
3246 key, self.xkey, self.xi = checkpattern(key, index, XPattern, self.xkey, self.xi)
3247 key, self.ykey, self.yi = checkpattern(key, index, YPattern, self.ykey, self.yi)
3248 key, self.xkey, self.xmini = checkpattern(key, index, XMinPattern, self.xkey, self.xmini)
3249 key, self.ykey, self.ymini = checkpattern(key, index, YMinPattern, self.ykey, self.ymini)
3250 key, self.xkey, self.xmaxi = checkpattern(key, index, XMaxPattern, self.xkey, self.xmaxi)
3251 key, self.ykey, self.ymaxi = checkpattern(key, index, YMaxPattern, self.ykey, self.ymaxi)
3252 key, self.xkey, self.dxi = checkpattern(key, index, DXPattern, self.xkey, self.dxi)
3253 key, self.ykey, self.dyi = checkpattern(key, index, DYPattern, self.ykey, self.dyi)
3254 key, self.xkey, self.dxmini = checkpattern(key, index, DXMinPattern, self.xkey, self.dxmini)
3255 key, self.ykey, self.dymini = checkpattern(key, index, DYMinPattern, self.ykey, self.dymini)
3256 key, self.xkey, self.dxmaxi = checkpattern(key, index, DXMaxPattern, self.xkey, self.dxmaxi)
3257 key, self.ykey, self.dymaxi = checkpattern(key, index, DYMaxPattern, self.ykey, self.dymaxi)
3258 if key is not None:
3259 self.othercolumnkey(key, index)
3260 if None in (self.xkey, self.ykey): raise ValueError("incomplete axis specification")
3261 if (len(filter(None, (self.xmini, self.dxmini, self.dxi))) > 1 or
3262 len(filter(None, (self.ymini, self.dymini, self.dyi))) > 1 or
3263 len(filter(None, (self.xmaxi, self.dxmaxi, self.dxi))) > 1 or
3264 len(filter(None, (self.ymaxi, self.dymaxi, self.dyi))) > 1):
3265 raise ValueError("multiple errorbar definition")
3266 if ((self.xi is None and self.dxi is not None) or
3267 (self.yi is None and self.dyi is not None) or
3268 (self.xi is None and self.dxmini is not None) or
3269 (self.yi is None and self.dymini is not None) or
3270 (self.xi is None and self.dxmaxi is not None) or
3271 (self.yi is None and self.dymaxi is not None)):
3272 raise ValueError("errorbar definition start value missing")
3273 self.xaxis = graph.axes[self.xkey]
3274 self.yaxis = graph.axes[self.ykey]
3276 def minmidmax(self, point, i, mini, maxi, di, dmini, dmaxi):
3277 min = max = mid = None
3278 try:
3279 mid = point[i] + 0.0
3280 except (TypeError, ValueError):
3281 pass
3282 try:
3283 if di is not None: min = point[i] - point[di]
3284 elif dmini is not None: min = point[i] - point[dmini]
3285 elif mini is not None: min = point[mini] + 0.0
3286 except (TypeError, ValueError):
3287 pass
3288 try:
3289 if di is not None: max = point[i] + point[di]
3290 elif dmaxi is not None: max = point[i] + point[dmaxi]
3291 elif maxi is not None: max = point[maxi] + 0.0
3292 except (TypeError, ValueError):
3293 pass
3294 if mid is not None:
3295 if min is not None and min > mid: raise ValueError("minimum error in errorbar")
3296 if max is not None and max < mid: raise ValueError("maximum error in errorbar")
3297 else:
3298 if min is not None and max is not None and min > max: raise ValueError("minimum/maximum error in errorbar")
3299 return min, mid, max
3301 def keyrange(self, points, i, mini, maxi, di, dmini, dmaxi):
3302 allmin = allmax = None
3303 if filter(None, (mini, maxi, di, dmini, dmaxi)) is not None:
3304 for point in points:
3305 min, mid, max = self.minmidmax(point, i, mini, maxi, di, dmini, dmaxi)
3306 if min is not None and (allmin is None or min < allmin): allmin = min
3307 if mid is not None and (allmin is None or mid < allmin): allmin = mid
3308 if mid is not None and (allmax is None or mid > allmax): allmax = mid
3309 if max is not None and (allmax is None or max > allmax): allmax = max
3310 else:
3311 for point in points:
3312 try:
3313 value = point[i] + 0.0
3314 if allmin is None or point[i] < allmin: allmin = point[i]
3315 if allmax is None or point[i] > allmax: allmax = point[i]
3316 except (TypeError, ValueError):
3317 pass
3318 return allmin, allmax
3320 def getranges(self, points):
3321 xmin, xmax = self.keyrange(points, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
3322 ymin, ymax = self.keyrange(points, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
3323 return {self.xkey: (xmin, xmax), self.ykey: (ymin, ymax)}
3325 def _drawerrorbar(self, graph, topleft, top, topright,
3326 left, center, right,
3327 bottomleft, bottom, bottomright, point=None):
3328 if left is not None:
3329 if right is not None:
3330 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3331 left2 = graph._addpos(*(left+(0, self._errorsize)))
3332 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3333 right2 = graph._addpos(*(right+(0, self._errorsize)))
3334 graph.stroke(path.path(path._moveto(*left1),
3335 graph._connect(*(left1+left2)),
3336 path._moveto(*left),
3337 graph._connect(*(left+right)),
3338 path._moveto(*right1),
3339 graph._connect(*(right1+right2))),
3340 *self.errorbarattrs)
3341 elif center is not None:
3342 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3343 left2 = graph._addpos(*(left+(0, self._errorsize)))
3344 graph.stroke(path.path(path._moveto(*left1),
3345 graph._connect(*(left1+left2)),
3346 path._moveto(*left),
3347 graph._connect(*(left+center))),
3348 *self.errorbarattrs)
3349 else:
3350 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3351 left2 = graph._addpos(*(left+(0, self._errorsize)))
3352 left3 = graph._addpos(*(left+(self._errorsize, 0)))
3353 graph.stroke(path.path(path._moveto(*left1),
3354 graph._connect(*(left1+left2)),
3355 path._moveto(*left),
3356 graph._connect(*(left+left3))),
3357 *self.errorbarattrs)
3358 if right is not None and left is None:
3359 if center is not None:
3360 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3361 right2 = graph._addpos(*(right+(0, self._errorsize)))
3362 graph.stroke(path.path(path._moveto(*right1),
3363 graph._connect(*(right1+right2)),
3364 path._moveto(*right),
3365 graph._connect(*(right+center))),
3366 *self.errorbarattrs)
3367 else:
3368 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3369 right2 = graph._addpos(*(right+(0, self._errorsize)))
3370 right3 = graph._addpos(*(right+(-self._errorsize, 0)))
3371 graph.stroke(path.path(path._moveto(*right1),
3372 graph._connect(*(right1+right2)),
3373 path._moveto(*right),
3374 graph._connect(*(right+right3))),
3375 *self.errorbarattrs)
3377 if bottom is not None:
3378 if top is not None:
3379 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3380 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3381 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3382 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3383 graph.stroke(path.path(path._moveto(*bottom1),
3384 graph._connect(*(bottom1+bottom2)),
3385 path._moveto(*bottom),
3386 graph._connect(*(bottom+top)),
3387 path._moveto(*top1),
3388 graph._connect(*(top1+top2))),
3389 *self.errorbarattrs)
3390 elif center is not None:
3391 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3392 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3393 graph.stroke(path.path(path._moveto(*bottom1),
3394 graph._connect(*(bottom1+bottom2)),
3395 path._moveto(*bottom),
3396 graph._connect(*(bottom+center))),
3397 *self.errorbarattrs)
3398 else:
3399 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3400 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3401 bottom3 = graph._addpos(*(bottom+(0, self._errorsize)))
3402 graph.stroke(path.path(path._moveto(*bottom1),
3403 graph._connect(*(bottom1+bottom2)),
3404 path._moveto(*bottom),
3405 graph._connect(*(bottom+bottom3))),
3406 *self.errorbarattrs)
3407 if top is not None and bottom is None:
3408 if center is not None:
3409 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3410 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3411 graph.stroke(path.path(path._moveto(*top1),
3412 graph._connect(*(top1+top2)),
3413 path._moveto(*top),
3414 graph._connect(*(top+center))),
3415 *self.errorbarattrs)
3416 else:
3417 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3418 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3419 top3 = graph._addpos(*(top+(0, -self._errorsize)))
3420 graph.stroke(path.path(path._moveto(*top1),
3421 graph._connect(*(top1+top2)),
3422 path._moveto(*top),
3423 graph._connect(*(top+top3))),
3424 *self.errorbarattrs)
3425 if bottomleft is not None:
3426 if topleft is not None and bottomright is None:
3427 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
3428 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
3429 graph.stroke(path.path(path._moveto(*bottomleft1),
3430 graph._connect(*(bottomleft1+bottomleft)),
3431 graph._connect(*(bottomleft+topleft)),
3432 graph._connect(*(topleft+topleft1))),
3433 *self.errorbarattrs)
3434 elif bottomright is not None and topleft is None:
3435 bottomleft1 = graph._addpos(*(bottomleft+(0, self._errorsize)))
3436 bottomright1 = graph._addpos(*(bottomright+(0, self._errorsize)))
3437 graph.stroke(path.path(path._moveto(*bottomleft1),
3438 graph._connect(*(bottomleft1+bottomleft)),
3439 graph._connect(*(bottomleft+bottomright)),
3440 graph._connect(*(bottomright+bottomright1))),
3441 *self.errorbarattrs)
3442 elif bottomright is None and topleft is None:
3443 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
3444 bottomleft2 = graph._addpos(*(bottomleft+(0, self._errorsize)))
3445 graph.stroke(path.path(path._moveto(*bottomleft1),
3446 graph._connect(*(bottomleft1+bottomleft)),
3447 graph._connect(*(bottomleft+bottomleft2))),
3448 *self.errorbarattrs)
3449 if topright is not None:
3450 if bottomright is not None and topleft is None:
3451 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
3452 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
3453 graph.stroke(path.path(path._moveto(*topright1),
3454 graph._connect(*(topright1+topright)),
3455 graph._connect(*(topright+bottomright)),
3456 graph._connect(*(bottomright+bottomright1))),
3457 *self.errorbarattrs)
3458 elif topleft is not None and bottomright is None:
3459 topright1 = graph._addpos(*(topright+(0, -self._errorsize)))
3460 topleft1 = graph._addpos(*(topleft+(0, -self._errorsize)))
3461 graph.stroke(path.path(path._moveto(*topright1),
3462 graph._connect(*(topright1+topright)),
3463 graph._connect(*(topright+topleft)),
3464 graph._connect(*(topleft+topleft1))),
3465 *self.errorbarattrs)
3466 elif topleft is None and bottomright is None:
3467 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
3468 topright2 = graph._addpos(*(topright+(0, -self._errorsize)))
3469 graph.stroke(path.path(path._moveto(*topright1),
3470 graph._connect(*(topright1+topright)),
3471 graph._connect(*(topright+topright2))),
3472 *self.errorbarattrs)
3473 if bottomright is not None and bottomleft is None and topright is None:
3474 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
3475 bottomright2 = graph._addpos(*(bottomright+(0, self._errorsize)))
3476 graph.stroke(path.path(path._moveto(*bottomright1),
3477 graph._connect(*(bottomright1+bottomright)),
3478 graph._connect(*(bottomright+bottomright2))),
3479 *self.errorbarattrs)
3480 if topleft is not None and bottomleft is None and topright is None:
3481 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
3482 topleft2 = graph._addpos(*(topleft+(0, -self._errorsize)))
3483 graph.stroke(path.path(path._moveto(*topleft1),
3484 graph._connect(*(topleft1+topleft)),
3485 graph._connect(*(topleft+topleft2))),
3486 *self.errorbarattrs)
3487 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
3488 graph.stroke(path.path(path._moveto(*bottomleft),
3489 graph._connect(*(bottomleft+bottomright)),
3490 graph._connect(*(bottomright+topright)),
3491 graph._connect(*(topright+topleft)),
3492 path.closepath()),
3493 *self.errorbarattrs)
3495 def _drawsymbol(self, canvas, x, y, point=None):
3496 canvas.draw(path.path(*self.symbol(self, x, y)), *self.symbolattrs)
3498 def drawsymbol(self, canvas, x, y, point=None):
3499 self._drawsymbol(canvas, unit.topt(x), unit.topt(y), point)
3501 def key(self, c, x, y, width, height):
3502 if self._symbolattrs is not None:
3503 self._drawsymbol(c, x + 0.5 * width, y + 0.5 * height)
3504 if self._lineattrs is not None:
3505 c.stroke(path._line(x, y + 0.5 * height, x + width, y + 0.5 * height), *self.lineattrs)
3507 def drawpoints(self, graph, points):
3508 xaxismin, xaxismax = self.xaxis.getdatarange()
3509 yaxismin, yaxismax = self.yaxis.getdatarange()
3510 self.size = unit.length(_getattr(self.size_str), default_type="v")
3511 self._size = unit.topt(self.size)
3512 self.symbol = _getattr(self._symbol)
3513 self.symbolattrs = _getattrs(helper.ensuresequence(self._symbolattrs))
3514 self.errorbarattrs = _getattrs(helper.ensuresequence(self._errorbarattrs))
3515 self._errorsize = self.errorscale * self._size
3516 self.errorsize = self.errorscale * self.size
3517 self.lineattrs = _getattrs(helper.ensuresequence(self._lineattrs))
3518 if self._lineattrs is not None:
3519 clipcanvas = graph.clipcanvas()
3520 lineels = []
3521 haserror = filter(None, (self.xmini, self.ymini, self.xmaxi, self.ymaxi,
3522 self.dxi, self.dyi, self.dxmini, self.dymini, self.dxmaxi, self.dymaxi)) is not None
3523 moveto = 1
3524 for point in points:
3525 drawsymbol = 1
3526 xmin, x, xmax = self.minmidmax(point, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
3527 ymin, y, ymax = self.minmidmax(point, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
3528 if x is not None and x < xaxismin: drawsymbol = 0
3529 elif x is not None and x > xaxismax: drawsymbol = 0
3530 elif y is not None and y < yaxismin: drawsymbol = 0
3531 elif y is not None and y > yaxismax: drawsymbol = 0
3532 elif haserror:
3533 if xmin is not None and xmin < xaxismin: drawsymbol = 0
3534 elif xmax is not None and xmax < xaxismin: drawsymbol = 0
3535 elif xmax is not None and xmax > xaxismax: drawsymbol = 0
3536 elif xmin is not None and xmin > xaxismax: drawsymbol = 0
3537 elif ymin is not None and ymin < yaxismin: drawsymbol = 0
3538 elif ymax is not None and ymax < yaxismin: drawsymbol = 0
3539 elif ymax is not None and ymax > yaxismax: drawsymbol = 0
3540 elif ymin is not None and ymin > yaxismax: drawsymbol = 0
3541 xpos=ypos=topleft=top=topright=left=center=right=bottomleft=bottom=bottomright=None
3542 if x is not None and y is not None:
3543 try:
3544 center = xpos, ypos = graph._pos(x, y, xaxis=self.xaxis, yaxis=self.yaxis)
3545 except (ValueError, OverflowError): # XXX: exceptions???
3546 pass
3547 if haserror:
3548 if y is not None:
3549 if xmin is not None: left = graph._pos(xmin, y, xaxis=self.xaxis, yaxis=self.yaxis)
3550 if xmax is not None: right = graph._pos(xmax, y, xaxis=self.xaxis, yaxis=self.yaxis)
3551 if x is not None:
3552 if ymax is not None: top = graph._pos(x, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3553 if ymin is not None: bottom = graph._pos(x, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3554 if x is None or y is None:
3555 if ymax is not None:
3556 if xmin is not None: topleft = graph._pos(xmin, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3557 if xmax is not None: topright = graph._pos(xmax, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3558 if ymin is not None:
3559 if xmin is not None: bottomleft = graph._pos(xmin, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3560 if xmax is not None: bottomright = graph._pos(xmax, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3561 if drawsymbol:
3562 if self._errorbarattrs is not None and haserror:
3563 self._drawerrorbar(graph, topleft, top, topright,
3564 left, center, right,
3565 bottomleft, bottom, bottomright, point)
3566 if self._symbolattrs is not None and xpos is not None and ypos is not None:
3567 self._drawsymbol(graph, xpos, ypos, point)
3568 if xpos is not None and ypos is not None:
3569 if moveto:
3570 lineels.append(path._moveto(xpos, ypos))
3571 moveto = 0
3572 else:
3573 lineels.append(path._lineto(xpos, ypos))
3574 else:
3575 moveto = 1
3576 self.path = path.path(*lineels)
3577 if self._lineattrs is not None:
3578 clipcanvas.stroke(self.path, *self.lineattrs)
3581 class changesymbol(changesequence): pass
3584 class _changesymbolcross(changesymbol):
3585 defaultsequence = (symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond)
3588 class _changesymbolplus(changesymbol):
3589 defaultsequence = (symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross)
3592 class _changesymbolsquare(changesymbol):
3593 defaultsequence = (symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus)
3596 class _changesymboltriangle(changesymbol):
3597 defaultsequence = (symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square)
3600 class _changesymbolcircle(changesymbol):
3601 defaultsequence = (symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle)
3604 class _changesymboldiamond(changesymbol):
3605 defaultsequence = (symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle)
3608 class _changesymbolsquaretwice(changesymbol):
3609 defaultsequence = (symbol.square, symbol.square, symbol.triangle, symbol.triangle,
3610 symbol.circle, symbol.circle, symbol.diamond, symbol.diamond)
3613 class _changesymboltriangletwice(changesymbol):
3614 defaultsequence = (symbol.triangle, symbol.triangle, symbol.circle, symbol.circle,
3615 symbol.diamond, symbol.diamond, symbol.square, symbol.square)
3618 class _changesymbolcircletwice(changesymbol):
3619 defaultsequence = (symbol.circle, symbol.circle, symbol.diamond, symbol.diamond,
3620 symbol.square, symbol.square, symbol.triangle, symbol.triangle)
3623 class _changesymboldiamondtwice(changesymbol):
3624 defaultsequence = (symbol.diamond, symbol.diamond, symbol.square, symbol.square,
3625 symbol.triangle, symbol.triangle, symbol.circle, symbol.circle)
3628 changesymbol.cross = _changesymbolcross
3629 changesymbol.plus = _changesymbolplus
3630 changesymbol.square = _changesymbolsquare
3631 changesymbol.triangle = _changesymboltriangle
3632 changesymbol.circle = _changesymbolcircle
3633 changesymbol.diamond = _changesymboldiamond
3634 changesymbol.squaretwice = _changesymbolsquaretwice
3635 changesymbol.triangletwice = _changesymboltriangletwice
3636 changesymbol.circletwice = _changesymbolcircletwice
3637 changesymbol.diamondtwice = _changesymboldiamondtwice
3640 class line(symbol):
3642 def __init__(self, lineattrs=helper.nodefault):
3643 if lineattrs is helper.nodefault:
3644 lineattrs = (changelinestyle(), canvas.linejoin.round)
3645 symbol.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
3648 class rect(symbol):
3650 def __init__(self, palette=color.palette.Gray):
3651 self.palette = palette
3652 self.colorindex = None
3653 symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
3655 def iterate(self):
3656 raise RuntimeError("style is not iterateable")
3658 def othercolumnkey(self, key, index):
3659 if key == "color":
3660 self.colorindex = index
3661 else:
3662 symbol.othercolumnkey(self, key, index)
3664 def _drawerrorbar(self, graph, topleft, top, topright,
3665 left, center, right,
3666 bottomleft, bottom, bottomright, point=None):
3667 color = point[self.colorindex]
3668 if color is not None:
3669 if color != self.lastcolor:
3670 self.rectclipcanvas.set(self.palette.getcolor(color))
3671 if bottom is not None and left is not None:
3672 bottomleft = left[0], bottom[1]
3673 if bottom is not None and right is not None:
3674 bottomright = right[0], bottom[1]
3675 if top is not None and right is not None:
3676 topright = right[0], top[1]
3677 if top is not None and left is not None:
3678 topleft = left[0], top[1]
3679 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
3680 self.rectclipcanvas.fill(path.path(path._moveto(*bottomleft),
3681 graph._connect(*(bottomleft+bottomright)),
3682 graph._connect(*(bottomright+topright)),
3683 graph._connect(*(topright+topleft)),
3684 path.closepath()))
3686 def drawpoints(self, graph, points):
3687 if self.colorindex is None:
3688 raise RuntimeError("column 'color' not set")
3689 self.lastcolor = None
3690 self.rectclipcanvas = graph.clipcanvas()
3691 symbol.drawpoints(self, graph, points)
3693 def key(self, c, x, y, width, height):
3694 raise RuntimeError("style doesn't yet provide a key")
3697 class text(symbol):
3699 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=textmodule.halign.center, **args):
3700 self.textindex = None
3701 self.textdx_str = textdx
3702 self.textdy_str = textdy
3703 self._textattrs = textattrs
3704 symbol.__init__(self, **args)
3706 def iteratedict(self):
3707 result = symbol.iteratedict()
3708 result["textattrs"] = _iterateattr(self._textattrs)
3709 return result
3711 def iterate(self):
3712 return textsymbol(**self.iteratedict())
3714 def othercolumnkey(self, key, index):
3715 if key == "text":
3716 self.textindex = index
3717 else:
3718 symbol.othercolumnkey(self, key, index)
3720 def _drawsymbol(self, graph, x, y, point=None):
3721 symbol._drawsymbol(self, graph, x, y, point)
3722 if None not in (x, y, point[self.textindex]) and self._textattrs is not None:
3723 graph._text(x + self._textdx, y + self._textdy, str(point[self.textindex]), *helper.ensuresequence(self.textattrs))
3725 def drawpoints(self, graph, points):
3726 self.textdx = unit.length(_getattr(self.textdx_str), default_type="v")
3727 self.textdy = unit.length(_getattr(self.textdy_str), default_type="v")
3728 self._textdx = unit.topt(self.textdx)
3729 self._textdy = unit.topt(self.textdy)
3730 if self._textattrs is not None:
3731 self.textattrs = _getattr(self._textattrs)
3732 if self.textindex is None:
3733 raise RuntimeError("column 'text' not set")
3734 symbol.drawpoints(self, graph, points)
3736 def key(self, c, x, y, width, height):
3737 raise RuntimeError("style doesn't yet provide a key")
3740 class arrow(symbol):
3742 def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
3743 self.linelength_str = linelength
3744 self.arrowsize_str = arrowsize
3745 self.arrowattrs = arrowattrs
3746 self.arrowdict = arrowdict
3747 self.epsilon = epsilon
3748 self.sizeindex = self.angleindex = None
3749 symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
3751 def iterate(self):
3752 raise RuntimeError("style is not iterateable")
3754 def othercolumnkey(self, key, index):
3755 if key == "size":
3756 self.sizeindex = index
3757 elif key == "angle":
3758 self.angleindex = index
3759 else:
3760 symbol.othercolumnkey(self, key, index)
3762 def _drawsymbol(self, graph, x, y, point=None):
3763 if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
3764 if point[self.sizeindex] > self.epsilon:
3765 dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
3766 x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
3767 y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
3768 x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
3769 y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
3770 graph.stroke(path.line(x1, y1, x2, y2),
3771 canvas.earrow(self.arrowsize*point[self.sizeindex],
3772 **self.arrowdict),
3773 *helper.ensuresequence(self.arrowattrs))
3775 def drawpoints(self, graph, points):
3776 self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
3777 self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
3778 self._arrowsize = unit.topt(self.arrowsize)
3779 self._linelength = unit.topt(self.linelength)
3780 if self.sizeindex is None:
3781 raise RuntimeError("column 'size' not set")
3782 if self.angleindex is None:
3783 raise RuntimeError("column 'angle' not set")
3784 symbol.drawpoints(self, graph, points)
3786 def key(self, c, x, y, width, height):
3787 raise RuntimeError("style doesn't yet provide a key")
3790 class _bariterator(changeattr):
3792 def attr(self, index):
3793 return index, self.counter
3796 class bar:
3798 def __init__(self, fromzero=1, stacked=0, skipmissing=1, xbar=0,
3799 barattrs=helper.nodefault, _usebariterator=helper.nodefault, _previousbar=None):
3800 self.fromzero = fromzero
3801 self.stacked = stacked
3802 self.skipmissing = skipmissing
3803 self.xbar = xbar
3804 if barattrs is helper.nodefault:
3805 self._barattrs = (canvas.stroked(color.gray.black), changecolor.Rainbow())
3806 else:
3807 self._barattrs = barattrs
3808 if _usebariterator is helper.nodefault:
3809 self.bariterator = _bariterator()
3810 else:
3811 self.bariterator = _usebariterator
3812 self.previousbar = _previousbar
3814 def iteratedict(self):
3815 result = {}
3816 result["barattrs"] = _iterateattrs(self._barattrs)
3817 return result
3819 def iterate(self):
3820 return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
3821 _usebariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
3823 def setcolumns(self, graph, columns):
3824 def checkpattern(key, index, pattern, iskey, isindex):
3825 if key is not None:
3826 match = pattern.match(key)
3827 if match:
3828 if isindex is not None: raise ValueError("multiple key specification")
3829 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
3830 key = None
3831 iskey = match.groups()[0]
3832 isindex = index
3833 return key, iskey, isindex
3835 xkey = ykey = None
3836 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
3837 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3838 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3839 xi = yi = None
3840 for key, index in columns.items():
3841 key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
3842 key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
3843 if key is not None:
3844 self.othercolumnkey(key, index)
3845 if None in (xkey, ykey): raise ValueError("incomplete axis specification")
3846 if self.xbar:
3847 self.nkey, self.ni = ykey, yi
3848 self.vkey, self.vi = xkey, xi
3849 else:
3850 self.nkey, self.ni = xkey, xi
3851 self.vkey, self.vi = ykey, yi
3852 self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
3854 def getranges(self, points):
3855 index, count = _getattr(self.bariterator)
3856 if count != 1 and self.stacked != 1:
3857 if self.stacked > 1:
3858 index = divmod(index, self.stacked)[0]
3860 vmin = vmax = None
3861 for point in points:
3862 if not self.skipmissing:
3863 if count != 1 and self.stacked != 1:
3864 self.naxis.setname(point[self.ni], index)
3865 else:
3866 self.naxis.setname(point[self.ni])
3867 try:
3868 v = point[self.vi] + 0.0
3869 if vmin is None or v < vmin: vmin = v
3870 if vmax is None or v > vmax: vmax = v
3871 except (TypeError, ValueError):
3872 pass
3873 else:
3874 if self.skipmissing:
3875 if count != 1 and self.stacked != 1:
3876 self.naxis.setname(point[self.ni], index)
3877 else:
3878 self.naxis.setname(point[self.ni])
3879 if self.fromzero:
3880 if vmin > 0: vmin = 0
3881 if vmax < 0: vmax = 0
3882 return {self.vkey: (vmin, vmax)}
3884 def drawpoints(self, graph, points):
3885 index, count = _getattr(self.bariterator)
3886 dostacked = (self.stacked != 0 and
3887 (self.stacked == 1 or divmod(index, self.stacked)[1]) and
3888 (self.stacked != 1 or index))
3889 if self.stacked > 1:
3890 index = divmod(index, self.stacked)[0]
3891 vmin, vmax = self.vaxis.getdatarange()
3892 self.barattrs = _getattrs(helper.ensuresequence(self._barattrs))
3893 if self.stacked:
3894 self.stackedvalue = {}
3895 for point in points:
3896 try:
3897 n = point[self.ni]
3898 v = point[self.vi]
3899 if self.stacked:
3900 self.stackedvalue[n] = v
3901 if count != 1 and self.stacked != 1:
3902 minid = (n, index, 0)
3903 maxid = (n, index, 1)
3904 else:
3905 minid = (n, 0)
3906 maxid = (n, 1)
3907 if self.xbar:
3908 x1pos, y1pos = graph._pos(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
3909 x2pos, y2pos = graph._pos(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
3910 else:
3911 x1pos, y1pos = graph._pos(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
3912 x2pos, y2pos = graph._pos(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
3913 if dostacked:
3914 if self.xbar:
3915 x3pos, y3pos = graph._pos(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
3916 x4pos, y4pos = graph._pos(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
3917 else:
3918 x3pos, y3pos = graph._pos(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
3919 x4pos, y4pos = graph._pos(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
3920 else:
3921 if self.fromzero:
3922 if self.xbar:
3923 x3pos, y3pos = graph._pos(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
3924 x4pos, y4pos = graph._pos(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
3925 else:
3926 x3pos, y3pos = graph._pos(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
3927 x4pos, y4pos = graph._pos(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
3928 else:
3929 x3pos, y3pos = self.naxis._vtickpoint(self.naxis, self.naxis.convert(maxid))
3930 x4pos, y4pos = self.naxis._vtickpoint(self.naxis, self.naxis.convert(minid))
3931 if self.barattrs is not None:
3932 graph.fill(path.path(path._moveto(x1pos, y1pos),
3933 graph._connect(x1pos, y1pos, x2pos, y2pos),
3934 graph._connect(x2pos, y2pos, x3pos, y3pos),
3935 graph._connect(x3pos, y3pos, x4pos, y4pos),
3936 graph._connect(x4pos, y4pos, x1pos, y1pos), # no closepath (might not be straight)
3937 path.closepath()), *self.barattrs)
3938 except (TypeError, ValueError): pass
3940 def key(self, c, x, y, width, height):
3941 c.fill(path._rect(x, y, width, height), *self.barattrs)
3944 #class surface:
3946 # def setcolumns(self, graph, columns):
3947 # self.columns = columns
3949 # def getranges(self, points):
3950 # return {"x": (0, 10), "y": (0, 10), "z": (0, 1)}
3952 # def drawpoints(self, graph, points):
3953 # pass
3957 ################################################################################
3958 # data
3959 ################################################################################
3962 class data:
3964 defaultstyle = symbol
3966 def __init__(self, file, title=helper.nodefault, context={}, **columns):
3967 self.title = title
3968 if helper.isstring(file):
3969 self.data = datamodule.datafile(file)
3970 else:
3971 self.data = file
3972 if title is helper.nodefault:
3973 self.title = "(unknown)"
3974 else:
3975 self.title = title
3976 self.columns = {}
3977 for key, column in columns.items():
3978 try:
3979 self.columns[key] = self.data.getcolumnno(column)
3980 except datamodule.ColumnError:
3981 self.columns[key] = len(self.data.titles)
3982 self.data.addcolumn(column, context=context)
3984 def setstyle(self, graph, style):
3985 self.style = style
3986 self.style.setcolumns(graph, self.columns)
3988 def getranges(self):
3989 return self.style.getranges(self.data.data)
3991 def setranges(self, ranges):
3992 pass
3994 def draw(self, graph):
3995 self.style.drawpoints(graph, self.data.data)
3998 class function:
4000 defaultstyle = line
4002 def __init__(self, expression, title=helper.nodefault, min=None, max=None, points=100, parser=mathtree.parser(), context={}):
4003 if title is helper.nodefault:
4004 self.title = expression
4005 else:
4006 self.title = title
4007 self.min = min
4008 self.max = max
4009 self.points = points
4010 self.context = context
4011 self.result, expression = expression.split("=")
4012 self.mathtree = parser.parse(expression)
4013 self.variable = None
4014 self.evalranges = 0
4016 def setstyle(self, graph, style):
4017 for variable in self.mathtree.VarList():
4018 if variable in graph.axes.keys():
4019 if self.variable is None:
4020 self.variable = variable
4021 else:
4022 raise ValueError("multiple variables found")
4023 if self.variable is None:
4024 raise ValueError("no variable found")
4025 self.xaxis = graph.axes[self.variable]
4026 self.style = style
4027 self.style.setcolumns(graph, {self.variable: 0, self.result: 1})
4029 def getranges(self):
4030 if self.evalranges:
4031 return self.style.getranges(self.data)
4032 if None not in (self.min, self.max):
4033 return {self.variable: (self.min, self.max)}
4035 def setranges(self, ranges):
4036 if ranges.has_key(self.variable):
4037 min, max = ranges[self.variable]
4038 if self.min is not None: min = self.min
4039 if self.max is not None: max = self.max
4040 vmin = self.xaxis.convert(min)
4041 vmax = self.xaxis.convert(max)
4042 self.data = []
4043 for i in range(self.points):
4044 self.context[self.variable] = x = self.xaxis.invert(vmin + (vmax-vmin)*i / (self.points-1.0))
4045 try:
4046 y = self.mathtree.Calc(**self.context)
4047 except (ArithmeticError, ValueError):
4048 y = None
4049 self.data.append((x, y))
4050 self.evalranges = 1
4052 def draw(self, graph):
4053 self.style.drawpoints(graph, self.data)
4056 class paramfunction:
4058 defaultstyle = line
4060 def __init__(self, varname, min, max, expression, title=helper.nodefault, points=100, parser=mathtree.parser(), context={}):
4061 if title is helper.nodefault:
4062 self.title = expression
4063 else:
4064 self.title = title
4065 self.varname = varname
4066 self.min = min
4067 self.max = max
4068 self.points = points
4069 self.expression = {}
4070 self.mathtrees = {}
4071 varlist, expressionlist = expression.split("=")
4072 parsestr = mathtree.ParseStr(expressionlist)
4073 for key in varlist.split(","):
4074 key = key.strip()
4075 if self.mathtrees.has_key(key):
4076 raise ValueError("multiple assignment in tuple")
4077 try:
4078 self.mathtrees[key] = parser.ParseMathTree(parsestr)
4079 break
4080 except mathtree.CommaFoundMathTreeParseError, e:
4081 self.mathtrees[key] = e.MathTree
4082 else:
4083 raise ValueError("unpack tuple of wrong size")
4084 if len(varlist.split(",")) != len(self.mathtrees.keys()):
4085 raise ValueError("unpack tuple of wrong size")
4086 self.data = []
4087 for i in range(self.points):
4088 context[self.varname] = self.min + (self.max-self.min)*i / (self.points-1.0)
4089 line = []
4090 for key, tree in self.mathtrees.items():
4091 line.append(tree.Calc(**context))
4092 self.data.append(line)
4094 def setstyle(self, graph, style):
4095 self.style = style
4096 columns = {}
4097 for key, index in zip(self.mathtrees.keys(), xrange(sys.maxint)):
4098 columns[key] = index
4099 self.style.setcolumns(graph, columns)
4101 def getranges(self):
4102 return self.style.getranges(self.data)
4104 def setranges(self, ranges):
4105 pass
4107 def draw(self, graph):
4108 self.style.drawpoints(graph, self.data)