basically finished all documentation; work on examples
[PyX/mjg.git] / pyx / graph.py
blobae42c494f31ef6d0a0fe7ad0dc4c7959f111f07a
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.text
178 When ticklevel or labellevel is None, no tick or label is present at that value.
179 When text is None, it should be automatically created (and stored), once the
180 an axis painter needs it."""
182 def __init__(self, enum, denom, ticklevel=None, labellevel=None, text=None):
183 frac.__init__(self, enum, denom)
184 self.ticklevel = ticklevel
185 self.labellevel = labellevel
186 self.text = text
188 def merge(self, other):
189 """merges two ticks together:
190 - the lower ticklevel/labellevel wins
191 - when present, self.text is taken over; otherwise the others text is taken
192 - the ticks should be at the same position (otherwise it doesn't make sense)
193 -> this is NOT checked
195 if self.ticklevel is None or (other.ticklevel is not None and other.ticklevel < self.ticklevel):
196 self.ticklevel = other.ticklevel
197 if self.labellevel is None or (other.labellevel is not None and other.labellevel < self.labellevel):
198 self.labellevel = other.labellevel
199 if self.text is None:
200 self.text = other.text
202 def __repr__(self):
203 return "tick(%r, %r, %s, %s, %s)" % (self.enum, self.denom, self.ticklevel, self.labellevel, self.text)
206 def _mergeticklists(list1, list2):
207 """helper function to merge tick lists
208 return a merged list of ticks out of list1 and list2
209 caution: original lists have to be ordered
210 (the returned list is also ordered)
211 caution: original lists are modified and they share references to
212 the result list!"""
213 # TODO: improve this using bisect
214 i = 0
215 j = 0
216 try:
217 while 1: # we keep on going until we reach an index error
218 while list2[j] < list1[i]: # insert tick
219 list1.insert(i, list2[j])
220 i += 1
221 j += 1
222 if list2[j] == list1[i]: # merge tick
223 list1[i].merge(list2[j])
224 j += 1
225 i += 1
226 except IndexError:
227 if j < len(list2):
228 list1 += list2[j:]
229 return list1
232 def _mergetexts(ticks, texts):
233 """helper function to merge texts into ticks
234 - when texts is not None, the text of all ticks with
235 labellevel
236 different from None are set
237 - texts need to be a sequence of sequences of strings,
238 where the first sequence contain the strings to be
239 used as texts for the ticks with labellevel 0,
240 the second sequence for labellevel 1, etc.
241 - when the maximum labellevel is 0, just a sequence of
242 strings might be provided as the texts argument
243 - IndexError is raised, when a sequence length doesn't match"""
244 if helper.issequenceofsequences(texts):
245 for text, level in zip(texts, xrange(sys.maxint)):
246 usetext = helper.ensuresequence(text)
247 i = 0
248 for tick in ticks:
249 if tick.labellevel == level:
250 tick.text = usetext[i]
251 i += 1
252 if i != len(usetext):
253 raise IndexError("wrong sequence length of texts at level %i" % level)
254 elif texts is not None:
255 usetext = helper.ensuresequence(texts)
256 i = 0
257 for tick in ticks:
258 if tick.labellevel == 0:
259 tick.text = usetext[i]
260 i += 1
261 if i != len(usetext):
262 raise IndexError("wrong sequence length of texts")
265 class _Ipart:
266 """interface definition of a partition scheme
267 partition schemes are used to create a list of ticks"""
269 def defaultpart(self, min, max, extendmin, extendmax):
270 """create a partition
271 - returns an ordered list of ticks for the interval min to max
272 - the interval is given in float numbers, thus an appropriate
273 conversion to rational numbers has to be performed
274 - extendmin and extendmax are booleans (integers)
275 - when extendmin or extendmax is set, the ticks might
276 extend the min-max range towards lower and higher
277 ranges, respectively"""
279 def lesspart(self):
280 """create another partition which contains less ticks
281 - this method is called several times after a call of defaultpart
282 - returns an ordered list of ticks with less ticks compared to
283 the partition returned by defaultpart and by previous calls
284 of lesspart
285 - the creation of a partition with strictly *less* ticks
286 is not to be taken serious
287 - the method might return None, when no other appropriate
288 partition can be created"""
291 def morepart(self):
292 """create another partition which contains more ticks
293 see lesspart, but increase the number of ticks"""
296 class manualpart:
297 """manual partition scheme
298 ticks and labels at positions explicitly provided to the constructor"""
300 __implements__ = _Ipart
302 def __init__(self, ticks=None, labels=None, texts=None, mix=()):
303 """configuration of the partition scheme
304 - ticks and labels should be a sequence of sequences, where
305 the first sequence contains the values to be used for
306 ticks with ticklevel/labellevel 0, the second sequence for
307 ticklevel/labellevel 1, etc.
308 - tick and label values must be frac instances or
309 strings convertable to fracs by the _ensurefrac function
310 - when the maximum ticklevel/labellevel is 0, just a sequence
311 might be provided in ticks and labels
312 - when labels is None and ticks is not None, the tick entries
313 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
314 - texts are applied to the resulting partition via the
315 mergetexts function (additional information available there)
316 - mix specifies another partition to be merged into the
317 created partition"""
318 if ticks is None and labels is not None:
319 self.ticks = helper.ensuresequence(helper.getsequenceno(labels, 0))
320 else:
321 self.ticks = ticks
322 if labels is None and ticks is not None:
323 self.labels = helper.ensuresequence(helper.getsequenceno(ticks, 0))
324 else:
325 self.labels = labels
326 self.texts = texts
327 self.mix = mix
329 def checkfraclist(self, *fracs):
330 """orders a list of fracs, equal entries are not allowed"""
331 if not len(fracs): return ()
332 sorted = list(fracs)
333 sorted.sort()
334 last = sorted[0]
335 for item in sorted[1:]:
336 if last == item:
337 raise ValueError("duplicate entry found")
338 last = item
339 return sorted
341 def part(self):
342 "create the partition as described in the constructor"
343 ticks = list(self.mix)
344 if helper.issequenceofsequences(self.ticks):
345 for fracs, level in zip(self.ticks, xrange(sys.maxint)):
346 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, ticklevel = level)
347 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(fracs)))])
348 else:
349 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, ticklevel = 0)
350 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(self.ticks)))])
352 if helper.issequenceofsequences(self.labels):
353 for fracs, level in zip(self.labels, xrange(sys.maxint)):
354 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, labellevel = level)
355 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(fracs)))])
356 else:
357 ticks = _mergeticklists(ticks, [tick(frac.enum, frac.denom, labellevel = 0)
358 for frac in self.checkfraclist(*map(_ensurefrac, helper.ensuresequence(self.labels)))])
360 _mergetexts(ticks, self.texts)
362 return ticks
364 def defaultpart(self, min, max, extendmin, extendmax):
365 """method is part of the implementation of _Ipart
366 XXX: we do not take care of the parameters -> correct?"""
367 return self.part()
369 def lesspart(self):
370 "method is part of the implementation of _Ipart"
371 return None
373 def morepart(self):
374 "method is part of the implementation of _Ipart"
375 return None
378 class linpart:
379 """linear partition scheme
380 ticks and label distances are explicitly provided to the constructor"""
382 __implements__ = _Ipart
384 def __init__(self, ticks=None, labels=None, texts=None, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
385 """configuration of the partition scheme
386 - ticks and labels should be a sequence, where the first value
387 is the distance between ticks with ticklevel/labellevel 0,
388 the second sequence for ticklevel/labellevel 1, etc.
389 - tick and label values must be frac instances or
390 strings convertable to fracs by the _ensurefrac function
391 - when the maximum ticklevel/labellevel is 0, just a single value
392 might be provided in ticks and labels
393 - when labels is None and ticks is not None, the tick entries
394 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
395 - texts are applied to the resulting partition via the
396 mergetexts function (additional information available there)
397 - extendtick allows for the extension of the range given to the
398 defaultpart method to include the next tick with the specified
399 level (None turns off this feature); note, that this feature is
400 also disabled, when an axis prohibits its range extension by
401 the extendmin/extendmax variables given to the defaultpart method
402 - extendlabel is analogous to extendtick, but for labels
403 - epsilon allows for exceeding the axis range by this relative
404 value (relative to the axis range given to the defaultpart method)
405 without creating another tick specified by extendtick/extendlabel
406 - mix specifies another partition to be merged into the
407 created partition"""
408 if ticks is None and labels is not None:
409 self.ticks = (_ensurefrac(helper.ensuresequence(labels)[0]),)
410 else:
411 self.ticks = map(_ensurefrac, helper.ensuresequence(ticks))
412 if labels is None and ticks is not None:
413 self.labels = (_ensurefrac(helper.ensuresequence(ticks)[0]),)
414 else:
415 self.labels = map(_ensurefrac, helper.ensuresequence(labels))
416 self.texts = texts
417 self.extendtick = extendtick
418 self.extendlabel = extendlabel
419 self.epsilon = epsilon
420 self.mix = mix
422 def extendminmax(self, min, max, frac, extendmin, extendmax):
423 """return new min, max tuple extending the range min, max
424 frac is the tick distance to be used
425 extendmin and extendmax are booleans to allow for the extension"""
426 if extendmin:
427 min = float(frac) * math.floor(min / float(frac) + self.epsilon)
428 if extendmax:
429 max = float(frac) * math.ceil(max / float(frac) - self.epsilon)
430 return min, max
432 def getticks(self, min, max, frac, ticklevel=None, labellevel=None):
433 """return a list of equal spaced ticks
434 - the tick distance is frac, the ticklevel is set to ticklevel and
435 the labellevel is set to labellevel
436 - min, max is the range where ticks should be placed"""
437 imin = int(math.ceil(min / float(frac) - 0.5 * self.epsilon))
438 imax = int(math.floor(max / float(frac) + 0.5 * self.epsilon))
439 ticks = []
440 for i in range(imin, imax + 1):
441 ticks.append(tick(long(i) * frac.enum, frac.denom, ticklevel=ticklevel, labellevel=labellevel))
442 return ticks
444 def defaultpart(self, min, max, extendmin, extendmax):
445 "method is part of the implementation of _Ipart"
446 if self.extendtick is not None and len(self.ticks) > self.extendtick:
447 min, max = self.extendminmax(min, max, self.ticks[self.extendtick], extendmin, extendmax)
448 if self.extendlabel is not None and len(self.labels) > self.extendlabel:
449 min, max = self.extendminmax(min, max, self.labels[self.extendlabel], extendmin, extendmax)
451 ticks = list(self.mix)
452 for i in range(len(self.ticks)):
453 ticks = _mergeticklists(ticks, self.getticks(min, max, self.ticks[i], ticklevel = i))
454 for i in range(len(self.labels)):
455 ticks = _mergeticklists(ticks, self.getticks(min, max, self.labels[i], labellevel = i))
457 _mergetexts(ticks, self.texts)
459 return ticks
461 def lesspart(self):
462 "method is part of the implementation of _Ipart"
463 return None
465 def morepart(self):
466 "method is part of the implementation of _Ipart"
467 return None
470 class autolinpart:
471 """automatic linear partition scheme
472 - possible tick distances are explicitly provided to the constructor
473 - tick distances are adjusted to the axis range by multiplication or division by 10"""
475 __implements__ = _Ipart
477 defaultlist = ((frac(1, 1), frac(1, 2)),
478 (frac(2, 1), frac(1, 1)),
479 (frac(5, 2), frac(5, 4)),
480 (frac(5, 1), frac(5, 2)))
482 def __init__(self, list=defaultlist, extendtick=0, epsilon=1e-10, mix=()):
483 """configuration of the partition scheme
484 - list should be a sequence of fracs
485 - ticks should be a sequence, where the first value
486 is the distance between ticks with ticklevel 0,
487 the second for ticklevel 1, etc.
488 - tick values must be frac instances or
489 strings convertable to fracs by the _ensurefrac function
490 - labellevel is set to None except for those ticks in the partitions,
491 where ticklevel is zero. There labellevel is also set to zero.
492 - extendtick allows for the extension of the range given to the
493 defaultpart method to include the next tick with the specified
494 level (None turns off this feature); note, that this feature is
495 also disabled, when an axis prohibits its range extension by
496 the extendmin/extendmax variables given to the defaultpart method
497 - epsilon allows for exceeding the axis range by this relative
498 value (relative to the axis range given to the defaultpart method)
499 without creating another tick specified by extendtick
500 - mix specifies another partition to be merged into the
501 created partition"""
502 self.list = list
503 self.extendtick = extendtick
504 self.epsilon = epsilon
505 self.mix = mix
507 def defaultpart(self, min, max, extendmin, extendmax):
508 "method is part of the implementation of _Ipart"
509 base = frac(10L, 1, int(math.log(max - min) / math.log(10)))
510 ticks = self.list[0]
511 useticks = [tick * base for tick in ticks]
512 self.lesstickindex = self.moretickindex = 0
513 self.lessbase = frac(base.enum, base.denom)
514 self.morebase = frac(base.enum, base.denom)
515 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
516 part = linpart(ticks=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
517 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
519 def lesspart(self):
520 "method is part of the implementation of _Ipart"
521 if self.lesstickindex < len(self.list) - 1:
522 self.lesstickindex += 1
523 else:
524 self.lesstickindex = 0
525 self.lessbase.enum *= 10
526 ticks = self.list[self.lesstickindex]
527 useticks = [tick * self.lessbase for tick in ticks]
528 part = linpart(ticks=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
529 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
531 def morepart(self):
532 "method is part of the implementation of _Ipart"
533 if self.moretickindex:
534 self.moretickindex -= 1
535 else:
536 self.moretickindex = len(self.list) - 1
537 self.morebase.denom *= 10
538 ticks = self.list[self.moretickindex]
539 useticks = [tick * self.morebase for tick in ticks]
540 part = linpart(ticks=useticks, extendtick=self.extendtick, epsilon=self.epsilon, mix=self.mix)
541 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
544 class shiftfracs:
545 """storage class for the definition of logarithmic axes partitions
546 instances of this class define tick positions suitable for
547 logarithmic axes by the following instance variables:
548 - shift: integer, which defines multiplicator
549 - fracs: list of tick positions (rational numbers, e.g. instances of frac)
550 possible positions are these tick positions and arbitrary divisions
551 and multiplications by the shift value"""
553 def __init__(self, shift, *fracs):
554 "create a shiftfracs instance and store its shift and fracs information"
555 self.shift = shift
556 self.fracs = fracs
559 class logpart(linpart):
560 """logarithmic partition scheme
561 ticks and label positions are explicitly provided to the constructor"""
563 __implements__ = _Ipart
565 shift5fracs1 = shiftfracs(100000, frac(1, 1))
566 shift4fracs1 = shiftfracs(10000, frac(1, 1))
567 shift3fracs1 = shiftfracs(1000, frac(1, 1))
568 shift2fracs1 = shiftfracs(100, frac(1, 1))
569 shiftfracs1 = shiftfracs(10, frac(1, 1))
570 shiftfracs125 = shiftfracs(10, frac(1, 1), frac(2, 1), frac(5, 1))
571 shiftfracs1to9 = shiftfracs(10, *map(lambda x: frac(x, 1), range(1, 10)))
572 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
574 def __init__(self, ticks=None, labels=None, texts=None, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
575 """configuration of the partition scheme
576 - ticks and labels should be a sequence, where the first value
577 is a shiftfracs instance describing ticks with ticklevel/labellevel 0,
578 the second sequence for ticklevel/labellevel 1, etc.
579 - when the maximum ticklevel/labellevel is 0, just a single
580 shiftfracs instance might be provided in ticks and labels
581 - when labels is None and ticks is not None, the tick entries
582 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
583 - texts are applied to the resulting partition via the
584 mergetexts function (additional information available there)
585 - extendtick allows for the extension of the range given to the
586 defaultpart method to include the next tick with the specified
587 level (None turns off this feature); note, that this feature is
588 also disabled, when an axis prohibits its range extension by
589 the extendmin/extendmax variables given to the defaultpart method
590 - extendlabel is analogous to extendtick, but for labels
591 - epsilon allows for exceeding the axis range by this relative
592 logarithm value (relative to the logarithm axis range given
593 to the defaultpart method) without creating another tick
594 specified by extendtick/extendlabel
595 - mix specifies another partition to be merged into the
596 created partition"""
597 if ticks is None and labels is not None:
598 self.ticks = (helper.ensuresequence(labels)[0],)
599 else:
600 self.ticks = helper.ensuresequence(ticks)
602 if labels is None and ticks is not None:
603 self.labels = (helper.ensuresequence(ticks)[0],)
604 else:
605 self.labels = helper.ensuresequence(labels)
606 self.texts = texts
607 self.extendtick = extendtick
608 self.extendlabel = extendlabel
609 self.epsilon = epsilon
610 self.mix = mix
612 def extendminmax(self, min, max, shiftfracs, extendmin, extendmax):
613 """return new min, max tuple extending the range min, max
614 shiftfracs describes the allowed tick positions
615 extendmin and extendmax are booleans to allow for the extension"""
616 minpower = None
617 maxpower = None
618 for i in xrange(len(shiftfracs.fracs)):
619 imin = int(math.floor(math.log(min / float(shiftfracs.fracs[i])) /
620 math.log(shiftfracs.shift) + self.epsilon)) + 1
621 imax = int(math.ceil(math.log(max / float(shiftfracs.fracs[i])) /
622 math.log(shiftfracs.shift) - self.epsilon)) - 1
623 if minpower is None or imin < minpower:
624 minpower, minindex = imin, i
625 if maxpower is None or imax >= maxpower:
626 maxpower, maxindex = imax, i
627 if minindex:
628 minfrac = shiftfracs.fracs[minindex - 1]
629 else:
630 minfrac = shiftfracs.fracs[-1]
631 minpower -= 1
632 if maxindex != len(shiftfracs.fracs) - 1:
633 maxfrac = shiftfracs.fracs[maxindex + 1]
634 else:
635 maxfrac = shiftfracs.fracs[0]
636 maxpower += 1
637 if extendmin:
638 min = float(minfrac) * float(shiftfracs.shift) ** minpower
639 if extendmax:
640 max = float(maxfrac) * float(shiftfracs.shift) ** maxpower
641 return min, max
643 def getticks(self, min, max, shiftfracs, ticklevel=None, labellevel=None):
644 """return a list of ticks
645 - shiftfracs describes the allowed tick positions
646 - the ticklevel of the ticks is set to ticklevel and
647 the labellevel is set to labellevel
648 - min, max is the range where ticks should be placed"""
649 ticks = list(self.mix)
650 minimin = 0
651 maximax = 0
652 for f in shiftfracs.fracs:
653 fracticks = []
654 imin = int(math.ceil(math.log(min / float(f)) /
655 math.log(shiftfracs.shift) - 0.5 * self.epsilon))
656 imax = int(math.floor(math.log(max / float(f)) /
657 math.log(shiftfracs.shift) + 0.5 * self.epsilon))
658 for i in range(imin, imax + 1):
659 pos = f * frac(shiftfracs.shift, 1, i)
660 fracticks.append(tick(pos.enum, pos.denom, ticklevel = ticklevel, labellevel = labellevel))
661 ticks = _mergeticklists(ticks, fracticks)
662 return ticks
665 class autologpart(logpart):
666 """automatic logarithmic partition scheme
667 possible tick positions are explicitly provided to the constructor"""
669 __implements__ = _Ipart
671 defaultlist = (((logpart.shiftfracs1, # ticks
672 logpart.shiftfracs1to9), # subticks
673 (logpart.shiftfracs1, # labels
674 logpart.shiftfracs125)), # sublevels
676 ((logpart.shiftfracs1, # ticks
677 logpart.shiftfracs1to9), # subticks
678 None), # labels like ticks
680 ((logpart.shift2fracs1, # ticks
681 logpart.shiftfracs1), # subticks
682 None), # labels like ticks
684 ((logpart.shift3fracs1, # ticks
685 logpart.shiftfracs1), # subticks
686 None), # labels like ticks
688 ((logpart.shift4fracs1, # ticks
689 logpart.shiftfracs1), # subticks
690 None), # labels like ticks
692 ((logpart.shift5fracs1, # ticks
693 logpart.shiftfracs1), # subticks
694 None)) # labels like ticks
696 def __init__(self, list=defaultlist, extendtick=0, extendlabel=None, epsilon=1e-10, mix=()):
697 """configuration of the partition scheme
698 - list should be a sequence of pairs of sequences of shiftfracs
699 instances
700 - within each pair the first sequence contains shiftfracs, where
701 the first shiftfracs instance describes ticks positions with
702 ticklevel 0, the second shiftfracs for ticklevel 1, etc.
703 - the second sequence within each pair describes the same as
704 before, but for labels
705 - within each pair: when the second entry (for the labels) is None
706 and the first entry (for the ticks) ticks is not None, the tick
707 entries for ticklevel 0 are used for labels and vice versa
708 (ticks<->labels)
709 - extendtick allows for the extension of the range given to the
710 defaultpart method to include the next tick with the specified
711 level (None turns off this feature); note, that this feature is
712 also disabled, when an axis prohibits its range extension by
713 the extendmin/extendmax variables given to the defaultpart method
714 - extendlabel is analogous to extendtick, but for labels
715 - epsilon allows for exceeding the axis range by this relative
716 logarithm value (relative to the logarithm axis range given
717 to the defaultpart method) without creating another tick
718 specified by extendtick/extendlabel
719 - mix specifies another partition to be merged into the
720 created partition"""
721 self.list = list
722 if len(list) > 2:
723 self.listindex = divmod(len(list), 2)[0]
724 else:
725 self.listindex = 0
726 self.extendtick = extendtick
727 self.extendlabel = extendlabel
728 self.epsilon = epsilon
729 self.mix = mix
731 def defaultpart(self, min, max, extendmin, extendmax):
732 "method is part of the implementation of _Ipart"
733 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
734 self.morelistindex = self.listindex
735 self.lesslistindex = self.listindex
736 part = logpart(ticks=self.list[self.listindex][0], labels=self.list[self.listindex][1],
737 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
738 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
740 def lesspart(self):
741 "method is part of the implementation of _Ipart"
742 self.lesslistindex += 1
743 if self.lesslistindex < len(self.list):
744 part = logpart(ticks=self.list[self.lesslistindex][0], labels=self.list[self.lesslistindex][1],
745 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
746 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
748 def morepart(self):
749 "method is part of the implementation of _Ipart"
750 self.morelistindex -= 1
751 if self.morelistindex >= 0:
752 part = logpart(ticks=self.list[self.morelistindex][0], labels=self.list[self.morelistindex][1],
753 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon, mix=self.mix)
754 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
758 ################################################################################
759 # rater
760 # conseptional remarks:
761 # - raters are used to calculate a rating for a realization of something
762 # - here, a rating means a positive floating point value
763 # - ratings are used to order those realizations by their suitability (lower
764 # ratings are better)
765 # - a rating of None means not suitable at all (those realizations should be
766 # thrown out)
767 ################################################################################
770 class cuberate:
771 """a cube rater
772 - a cube rater has an optimal value, where the rate becomes zero
773 - for a left (below the optimum) and a right value (above the optimum),
774 the rating is value is set to 1 (modified by an overall weight factor
775 for the rating)
776 - the analytic form of the rating is cubic for both, the left and
777 the right side of the rater, independently"""
779 def __init__(self, opt, left=None, right=None, weight=1):
780 """initializes the rater
781 - by default, left is set to zero, right is set to 3*opt
782 - left should be smaller than opt, right should be bigger than opt
783 - weight should be positive and is a factor multiplicated to the rates"""
784 if left is None:
785 left = 0
786 if right is None:
787 right = 3*opt
788 self.opt = opt
789 self.left = left
790 self.right = right
791 self.weight = weight
793 def rate(self, value, dense=1):
794 """returns a rating for a value
795 - the dense factor lineary rescales the rater (the optimum etc.),
796 e.g. a value bigger than one increases the optimum (when it is
797 positive) and a value lower than one decreases the optimum (when
798 it is positive); the dense factor itself should be positive"""
799 opt = self.opt * dense
800 if value < opt:
801 other = self.left * dense
802 elif value > opt:
803 other = self.right * dense
804 else:
805 return 0
806 factor = (value - opt) / float(other - opt)
807 return self.weight * (factor ** 3)
810 class distancerate:
811 """a distance rater (rates a list of distances)
812 - the distance rater rates a list of distances by rating each independently
813 and returning the average rate
814 - there is an optimal value, where the rate becomes zero
815 - the analytic form is linary for values above the optimal value
816 (twice the optimal value has the rating one, three times the optimal
817 value has the rating two, etc.)
818 - the analytic form is reciprocal subtracting one for values below the
819 optimal value (halve the optimal value has the rating one, one third of
820 the optimal value has the rating two, etc.)"""
822 def __init__(self, opt, weight=0.1):
823 """inititializes the rater
824 - opt is the optimal length (a PyX length, by default a visual length)
825 - weight should be positive and is a factor multiplicated to the rates"""
826 self.opt_str = opt
827 self.weight = weight
829 def _rate(self, distances, dense=1):
830 """rate distances
831 - the distances are a sequence of positive floats in PostScript points
832 - the dense factor lineary rescales the rater (the optimum etc.),
833 e.g. a value bigger than one increases the optimum (when it is
834 positive) and a value lower than one decreases the optimum (when
835 it is positive); the dense factor itself should be positive"""
836 if len(distances):
837 opt = unit.topt(unit.length(self.opt_str, default_type="v")) / dense
838 rate = 0
839 for distance in distances:
840 if distance < opt:
841 rate += self.weight * (opt / distance - 1)
842 else:
843 rate += self.weight * (distance / opt - 1)
844 return rate / float(len(distances))
847 class axisrater:
848 """a rater for axis partitions
849 - the rating of axes is splited into two separate parts:
850 - rating of the partitions in terms of the number of ticks,
851 subticks, labels, etc.
852 - rating of the label distances
853 - in the end, a rate for an axis partition is the sum of these rates
854 - it is useful to first just rate the number of ticks etc.
855 and selecting those partitions, where this fits well -> as soon
856 as an complete rate (the sum of both parts from the list above)
857 of a first partition is below a rate of just the ticks of another
858 partition, this second partition will never be better than the
859 first one -> we gain speed by minimizing the number of partitions,
860 where label distances have to be taken into account)
861 - both parts of the rating are shifted into instances of raters
862 defined above --- right now, there is not yet a strict interface
863 for this delegation (should be done as soon as it is needed)"""
865 linticks = (cuberate(4), cuberate(10, weight=0.5), )
866 linlabels = (cuberate(4), )
867 logticks = (cuberate(5, right=20), cuberate(20, right=100, weight=0.5), )
868 loglabels = (cuberate(5, right=20), cuberate(5, left=-20, right=20, weight=0.5), )
869 stdtickrange = cuberate(1, weight=2)
870 stddistance = distancerate("1 cm")
872 def __init__(self, ticks=linticks, labels=linlabels, tickrange=stdtickrange, distance=stddistance):
873 """initializes the axis rater
874 - ticks and labels are lists of instances of cuberate
875 - the first entry in ticks rate the number of ticks, the
876 second the number of subticks, etc.; when there are no
877 ticks of a level or there is not rater for a level, the
878 level is just ignored
879 - labels is analogous, but for labels
880 - within the rating, all ticks with a higher level are
881 considered as ticks for a given level
882 - tickrange is a cuberate instance, which rates the covering
883 of an axis range by the ticks (as a relative value of the
884 tick range vs. the axis range), ticks might cover less or
885 more than the axis range (for the standard automatic axis
886 partition schemes an extention of the axis range is normal
887 and should get some penalty)
888 - distance is an distancerate instance"""
889 self.ticks = ticks
890 self.labels = labels
891 self.tickrange = tickrange
892 self.distance = distance
894 def ratepart(self, axis, part, dense=1):
895 """rates a partition by some global parameters
896 - takes into account the number of ticks, subticks, etc.,
897 number of labels, etc., and the coverage of the axis
898 range by the ticks
899 - when there are no ticks of a level or there was not rater
900 given in the constructor for a level, the level is just
901 ignored
902 - the method returns the sum of the rating results divided
903 by the sum of the weights of the raters
904 - within the rating, all ticks with a higher level are
905 considered as ticks for a given level"""
906 tickslen = len(self.ticks)
907 labelslen = len(self.labels)
908 ticks = [0]*tickslen
909 labels = [0]*labelslen
910 if part is not None:
911 for tick in part:
912 if tick.ticklevel is not None:
913 for level in xrange(tick.ticklevel, tickslen):
914 ticks[level] += 1
915 if tick.labellevel is not None:
916 for level in xrange(tick.labellevel, labelslen):
917 labels[level] += 1
918 rate = 0
919 weight = 0
920 for tick, rater in zip(ticks, self.ticks):
921 rate += rater.rate(tick, dense=dense)
922 weight += rater.weight
923 for label, rater in zip(labels, self.labels):
924 rate += rater.rate(label, dense=dense)
925 weight += rater.weight
926 if part is not None and len(part):
927 tickmin, tickmax = axis.gettickrange() # XXX: tickrange was not yet applied!?
928 rate += self.tickrange.rate((float(part[-1]) - float(part[0])) * axis.divisor / (tickmax - tickmin))
929 else:
930 rate += self.tickrange.rate(0)
931 weight += self.tickrange.weight
932 return rate/weight
934 def _ratedistances(self, distances, dense=1):
935 """rate distances
936 - the distances should be collected as box distances of
937 subsequent labels (of any level))
938 - the distances are a sequence of positive floats in
939 PostScript points
940 - the dense factor is used within the distancerate instance"""
941 return self.distance._rate(distances, dense=dense)
944 ################################################################################
945 # axis painter
946 ################################################################################
949 class layoutdata: pass
952 class axistitlepainter:
954 paralleltext = -90
955 orthogonaltext = 0
957 def __init__(self, titledist="0.3 cm",
958 titleattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
959 titledirection=-90,
960 titlepos=0.5):
961 self.titledist_str = titledist
962 self.titleattrs = titleattrs
963 self.titledirection = titledirection
964 self.titlepos = titlepos
966 def reldirection(self, direction, dx, dy, epsilon=1e-10):
967 direction += math.atan2(dy, dx) * 180 / math.pi
968 while (direction > 90 + epsilon):
969 direction -= 180
970 while (direction < -90 - epsilon):
971 direction += 180
972 return direction
974 def dolayout(self, graph, axis):
975 if axis.title is not None and self.titleattrs is not None:
976 titledist = unit.topt(unit.length(self.titledist_str, default_type="v"))
977 x, y = axis._vtickpoint(axis, self.titlepos)
978 dx, dy = axis.vtickdirection(axis, self.titlepos)
979 # no not modify self.titleattrs ... the painter might be used by several axes!!!
980 titleattrs = list(helper.ensuresequence(self.titleattrs))
981 if self.titledirection is not None:
982 titleattrs = titleattrs + [trafo.rotate(self.reldirection(self.titledirection, dx, dy))]
983 axis.layoutdata.titlebox = graph.texrunner._text(x, y, axis.title, *titleattrs)
984 axis.layoutdata._extent += titledist
985 axis.layoutdata.titlebox._linealign(axis.layoutdata._extent, dx, dy)
986 axis.layoutdata._extent += axis.layoutdata.titlebox._extent(dx, dy)
987 else:
988 axis.layoutdata.titlebox = None
990 def paint(self, graph, axis):
991 if axis.layoutdata.titlebox is not None:
992 graph.insert(axis.layoutdata.titlebox)
995 class axispainter(axistitlepainter):
997 defaultticklengths = ["%0.5f cm" % (0.2*goldenmean**(-i)) for i in range(10)]
999 fractypeauto = 1
1000 fractyperat = 2
1001 fractypedec = 3
1002 fractypeexp = 4
1004 def __init__(self, innerticklengths=defaultticklengths,
1005 outerticklengths=None,
1006 tickattrs=(),
1007 gridattrs=None,
1008 zerolineattrs=(),
1009 baselineattrs=canvas.linecap.square,
1010 labeldist="0.3 cm",
1011 labelattrs=((textmodule.halign.center, textmodule.vshift.mathaxis),
1012 (textmodule.size.footnotesize, textmodule.halign.center, textmodule.vshift.mathaxis)),
1013 labeldirection=None,
1014 labelhequalize=0,
1015 labelvequalize=1,
1016 fractype=fractypeauto,
1017 ratfracsuffixenum=1,
1018 ratfracover=r"\over",
1019 decfracpoint=".",
1020 decfracequal=0,
1021 expfractimes=r"\cdot",
1022 expfracpre1=0,
1023 expfracminexp=4,
1024 suffix0=0,
1025 suffix1=0,
1026 **args):
1027 self.innerticklengths_str = innerticklengths
1028 self.outerticklengths_str = outerticklengths
1029 self.tickattrs = tickattrs
1030 self.gridattrs = gridattrs
1031 self.zerolineattrs = zerolineattrs
1032 self.baselineattrs = baselineattrs
1033 self.labeldist_str = labeldist
1034 self.labelattrs = labelattrs
1035 self.labeldirection = labeldirection
1036 self.labelhequalize = labelhequalize
1037 self.labelvequalize = labelvequalize
1038 self.fractype = fractype
1039 self.ratfracsuffixenum = ratfracsuffixenum
1040 self.ratfracover = ratfracover
1041 self.decfracpoint = decfracpoint
1042 self.decfracequal = decfracequal
1043 self.expfractimes = expfractimes
1044 self.expfracpre1 = expfracpre1
1045 self.expfracminexp = expfracminexp
1046 self.suffix0 = suffix0
1047 self.suffix1 = suffix1
1048 axistitlepainter.__init__(self, **args)
1050 def gcd(self, m, n):
1051 # greates common divisor, m & n must be non-negative
1052 if m < n:
1053 m, n = n, m
1054 while n > 0:
1055 m, (dummy, n) = n, divmod(m, n)
1056 return m
1058 def attachsuffix(self, tick, str):
1059 if self.suffix0 or tick.enum:
1060 if tick.suffix is not None and not self.suffix1:
1061 if str == "1":
1062 str = ""
1063 elif str == "-1":
1064 str = "-"
1065 if tick.suffix is not None:
1066 str = str + tick.suffix
1067 return str
1069 def ratfrac(self, tick):
1070 m, n = tick.enum, tick.denom
1071 sign = 1
1072 if m < 0: m, sign = -m, -sign
1073 if n < 0: n, sign = -n, -sign
1074 gcd = self.gcd(m, n)
1075 (m, dummy1), (n, dummy2) = divmod(m, gcd), divmod(n, gcd)
1076 if n != 1:
1077 if self.ratfracsuffixenum:
1078 if sign == -1:
1079 return "-{{%s}%s{%s}}" % (self.attachsuffix(tick, str(m)), self.ratfracover, n)
1080 else:
1081 return "{{%s}%s{%s}}" % (self.attachsuffix(tick, str(m)), self.ratfracover, n)
1082 else:
1083 if sign == -1:
1084 return self.attachsuffix(tick, "-{{%s}%s{%s}}" % (m, self.ratfracover, n))
1085 else:
1086 return self.attachsuffix(tick, "{{%s}%s{%s}}" % (m, self.ratfracover, n))
1087 else:
1088 if sign == -1:
1089 return self.attachsuffix(tick, "-%s" % m)
1090 else:
1091 return self.attachsuffix(tick, "%s" % m)
1093 def decfrac(self, tick, decfraclength=None):
1094 m, n = tick.enum, tick.denom
1095 sign = 1
1096 if m < 0: m, sign = -m, -sign
1097 if n < 0: n, sign = -n, -sign
1098 gcd = self.gcd(m, n)
1099 (m, dummy1), (n, dummy2) = divmod(m, gcd), divmod(n, gcd)
1100 frac, rest = divmod(m, n)
1101 strfrac = str(frac)
1102 rest = m % n
1103 if rest:
1104 strfrac += self.decfracpoint
1105 oldrest = []
1106 tick.decfraclength = 0
1107 while (rest):
1108 tick.decfraclength += 1
1109 if rest in oldrest:
1110 periodstart = len(strfrac) - (len(oldrest) - oldrest.index(rest))
1111 strfrac = strfrac[:periodstart] + r"\overline{" + strfrac[periodstart:] + "}"
1112 break
1113 oldrest += [rest]
1114 rest *= 10
1115 frac, rest = divmod(rest, n)
1116 strfrac += str(frac)
1117 else:
1118 if decfraclength is not None:
1119 while tick.decfraclength < decfraclength:
1120 strfrac += "0"
1121 tick.decfraclength += 1
1122 if sign == -1:
1123 return self.attachsuffix(tick, "-%s" % strfrac)
1124 else:
1125 return self.attachsuffix(tick, strfrac)
1127 def expfrac(self, tick, minexp = None):
1128 m, n = tick.enum, tick.denom
1129 sign = 1
1130 if m < 0: m, sign = -m, -sign
1131 if n < 0: n, sign = -n, -sign
1132 exp = 0
1133 if m:
1134 while divmod(m, n)[0] > 9:
1135 n *= 10
1136 exp += 1
1137 while divmod(m, n)[0] < 1:
1138 m *= 10
1139 exp -= 1
1140 if minexp is not None and ((exp < 0 and -exp < minexp) or (exp >= 0 and exp < minexp)):
1141 return None
1142 dummy = frac(m, n)
1143 dummy.suffix = None
1144 prefactor = self.decfrac(dummy)
1145 if prefactor == "1" and not self.expfracpre1:
1146 if sign == -1:
1147 return self.attachsuffix(tick, "-10^{%i}" % exp)
1148 else:
1149 return self.attachsuffix(tick, "10^{%i}" % exp)
1150 else:
1151 if sign == -1:
1152 return self.attachsuffix(tick, "-%s%s10^{%i}" % (prefactor, self.expfractimes, exp))
1153 else:
1154 return self.attachsuffix(tick, "%s%s10^{%i}" % (prefactor, self.expfractimes, exp))
1156 def createtext(self, tick):
1157 tick.decfraclength = None
1158 if self.fractype == self.fractypeauto:
1159 if tick.suffix is not None:
1160 tick.text = self.ratfrac(tick)
1161 else:
1162 tick.text = self.expfrac(tick, self.expfracminexp)
1163 if tick.text is None:
1164 tick.text = self.decfrac(tick)
1165 elif self.fractype == self.fractypedec:
1166 tick.text = self.decfrac(tick)
1167 elif self.fractype == self.fractypeexp:
1168 tick.text = self.expfrac(tick)
1169 elif self.fractype == self.fractyperat:
1170 tick.text = self.ratfrac(tick)
1171 else:
1172 raise ValueError("fractype invalid")
1173 if textmodule.mathmode not in tick.labelattrs:
1174 tick.labelattrs.append(textmodule.mathmode)
1176 def dolayout(self, graph, axis):
1177 labeldist = unit.topt(unit.length(self.labeldist_str, default_type="v"))
1178 for tick in axis.ticks:
1179 tick.virtual = axis.convert(float(tick) * axis.divisor)
1180 tick.x, tick.y = axis._vtickpoint(axis, tick.virtual)
1181 tick.dx, tick.dy = axis.vtickdirection(axis, tick.virtual)
1182 for tick in axis.ticks:
1183 tick.textbox = None
1184 if tick.labellevel is not None:
1185 tick.labelattrs = helper.getsequenceno(self.labelattrs, tick.labellevel)
1186 if tick.labelattrs is not None:
1187 tick.labelattrs = list(helper.ensuresequence(tick.labelattrs))
1188 if tick.text is None:
1189 tick.suffix = axis.suffix
1190 self.createtext(tick)
1191 if self.labeldirection is not None:
1192 tick.labelattrs += [trafo.rotate(self.reldirection(self.labeldirection, tick.dx, tick.dy))]
1193 tick.textbox = textmodule._text(tick.x, tick.y, tick.text, *tick.labelattrs)
1194 if self.decfracequal:
1195 maxdecfraclength = max([tick.decfraclength for tick in axis.ticks if tick.labellevel is not None and
1196 tick.labelattrs is not None and
1197 tick.decfraclength is not None])
1198 for tick in axis.ticks:
1199 if (tick.labellevel is not None and
1200 tick.labelattrs is not None and
1201 tick.decfraclength is not None):
1202 tick.text = self.decfrac(tick, maxdecfraclength)
1203 for tick in axis.ticks:
1204 if tick.labellevel is not None and tick.labelattrs is not None:
1205 tick.textbox = textmodule._text(tick.x, tick.y, tick.text, *tick.labelattrs)
1206 if len(axis.ticks) > 1:
1207 equaldirection = 1
1208 for tick in axis.ticks[1:]:
1209 if tick.dx != axis.ticks[0].dx or tick.dy != axis.ticks[0].dy:
1210 equaldirection = 0
1211 else:
1212 equaldirection = 0
1213 if equaldirection and ((not axis.ticks[0].dx and self.labelvequalize) or
1214 (not axis.ticks[0].dy and self.labelhequalize)):
1215 box._linealignequal([tick.textbox for tick in axis.ticks if tick.textbox],
1216 labeldist, axis.ticks[0].dx, axis.ticks[0].dy)
1217 else:
1218 for tick in axis.ticks:
1219 if tick.textbox:
1220 tick.textbox._linealign(labeldist, tick.dx, tick.dy)
1221 def topt_v_recursive(arg):
1222 if helper.issequence(arg):
1223 # return map(topt_v_recursive, arg) needs python2.2
1224 return [unit.topt(unit.length(a, default_type="v")) for a in arg]
1225 else:
1226 if arg is not None:
1227 return unit.topt(unit.length(arg, default_type="v"))
1228 innerticklengths = topt_v_recursive(self.innerticklengths_str)
1229 outerticklengths = topt_v_recursive(self.outerticklengths_str)
1230 axis.layoutdata._extent = 0
1231 for tick in axis.ticks:
1232 if tick.ticklevel is not None:
1233 tick.innerticklength = helper.getitemno(innerticklengths, tick.ticklevel)
1234 tick.outerticklength = helper.getitemno(outerticklengths, tick.ticklevel)
1235 if tick.innerticklength is not None and tick.outerticklength is None:
1236 tick.outerticklength = 0
1237 if tick.outerticklength is not None and tick.innerticklength is None:
1238 tick.innerticklength = 0
1239 extent = 0
1240 if tick.textbox is None:
1241 if tick.outerticklength is not None and tick.outerticklength > 0:
1242 extent = tick.outerticklength
1243 else:
1244 extent = tick.textbox._extent(tick.dx, tick.dy) + labeldist
1245 if axis.layoutdata._extent < extent:
1246 axis.layoutdata._extent = extent
1247 axistitlepainter.dolayout(self, graph, axis)
1249 def ratelayout(self, graph, axis, dense=1):
1250 ticktextboxes = [tick.textbox for tick in axis.ticks if tick.textbox is not None]
1251 if len(ticktextboxes) > 1:
1252 try:
1253 distances = [ticktextboxes[i]._boxdistance(ticktextboxes[i+1]) for i in range(len(ticktextboxes) - 1)]
1254 except box.BoxCrossError:
1255 return None
1256 rate = axis.rate._ratedistances(distances, dense)
1257 return rate
1258 else:
1259 if self.labelattrs is None:
1260 return 0
1262 def paint(self, graph, axis):
1263 for tick in axis.ticks:
1264 if tick.ticklevel is not None:
1265 if tick != frac(0, 1) or self.zerolineattrs is None:
1266 gridattrs = helper.getsequenceno(self.gridattrs, tick.ticklevel)
1267 if gridattrs is not None:
1268 graph.stroke(axis.vgridpath(tick.virtual), *helper.ensuresequence(gridattrs))
1269 tickattrs = helper.getsequenceno(self.tickattrs, tick.ticklevel)
1270 if None not in (tick.innerticklength, tick.outerticklength, tickattrs):
1271 x1 = tick.x - tick.dx * tick.innerticklength
1272 y1 = tick.y - tick.dy * tick.innerticklength
1273 x2 = tick.x + tick.dx * tick.outerticklength
1274 y2 = tick.y + tick.dy * tick.outerticklength
1275 graph.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(tickattrs))
1276 if tick.textbox is not None:
1277 graph.insert(tick.textbox)
1278 if self.baselineattrs is not None:
1279 graph.stroke(axis.vbaseline(axis), *helper.ensuresequence(self.baselineattrs))
1280 if self.zerolineattrs is not None:
1281 if len(axis.ticks) and axis.ticks[0] * axis.ticks[-1] < frac(0, 1):
1282 graph.stroke(axis.vgridpath(axis.convert(0)), *helper.ensuresequence(self.zerolineattrs))
1283 axistitlepainter.paint(self, graph, axis)
1286 class splitaxispainter(axistitlepainter):
1288 def __init__(self, breaklinesdist=0.05,
1289 breaklineslength=0.5,
1290 breaklinesangle=-60,
1291 breaklinesattrs=(),
1292 **args):
1293 self.breaklinesdist_str = breaklinesdist
1294 self.breaklineslength_str = breaklineslength
1295 self.breaklinesangle = breaklinesangle
1296 self.breaklinesattrs = breaklinesattrs
1297 axistitlepainter.__init__(self, **args)
1299 def subvbaseline(self, axis, v1=None, v2=None):
1300 if v1 is None:
1301 if self.breaklinesattrs is None:
1302 left = axis.vmin
1303 else:
1304 if axis.vminover is None:
1305 left = None
1306 else:
1307 left = axis.vminover
1308 else:
1309 left = axis.vmin+v1*(axis.vmax-axis.vmin)
1310 if v2 is None:
1311 if self.breaklinesattrs is None:
1312 right = axis.vmax
1313 else:
1314 if axis.vmaxover is None:
1315 right = None
1316 else:
1317 right = axis.vmaxover
1318 else:
1319 right = axis.vmin+v2*(axis.vmax-axis.vmin)
1320 return axis.baseaxis.vbaseline(axis.baseaxis, left, right)
1322 def dolayout(self, graph, axis):
1323 if self.breaklinesattrs is not None:
1324 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1325 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1326 self._breaklinesdist = unit.topt(self.breaklinesdist)
1327 self._breaklineslength = unit.topt(self.breaklineslength)
1328 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1329 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1330 axis.layoutdata._extent = (math.fabs(0.5 * self._breaklinesdist * self.cos) +
1331 math.fabs(0.5 * self._breaklineslength * self.sin))
1332 else:
1333 axis.layoutdata._extent = 0
1334 for subaxis in axis.axislist:
1335 subaxis.baseaxis = axis
1336 subaxis._vtickpoint = lambda axis, v: axis.baseaxis._vtickpoint(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1337 subaxis.vtickdirection = lambda axis, v: axis.baseaxis.vtickdirection(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1338 subaxis.vbaseline = self.subvbaseline
1339 subaxis.dolayout(graph)
1340 if axis.layoutdata._extent < subaxis.layoutdata._extent:
1341 axis.layoutdata._extent = subaxis.layoutdata._extent
1342 axistitlepainter.dolayout(self, graph, axis)
1344 def paint(self, graph, axis):
1345 for subaxis in axis.axislist:
1346 subaxis.dopaint(graph)
1347 if self.breaklinesattrs is not None:
1348 for subaxis1, subaxis2 in zip(axis.axislist[:-1], axis.axislist[1:]):
1349 # use a tangent of the baseline (this is independent of the tickdirection)
1350 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1351 breakline = path.normpath(axis.vbaseline(axis, v, None)).tangent(0, self.breaklineslength)
1352 widthline = path.normpath(axis.vbaseline(axis, v, None)).tangent(0, self.breaklinesdist).transformed(trafo.rotate(self.breaklinesangle+90, *breakline.begin()))
1353 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1354 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1355 breakline = breakline.transformed(trafo.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1356 breakline1 = breakline.transformed(trafo.translate(*towidth))
1357 breakline2 = breakline.transformed(trafo.translate(-towidth[0], -towidth[1]))
1358 graph.fill(path.path(path.moveto(*breakline1.begin()),
1359 path.lineto(*breakline1.end()),
1360 path.lineto(*breakline2.end()),
1361 path.lineto(*breakline2.begin()),
1362 path.closepath()), color.gray.white)
1363 graph.stroke(breakline1, *helper.ensuresequence(self.breaklinesattrs))
1364 graph.stroke(breakline2, *helper.ensuresequence(self.breaklinesattrs))
1365 axistitlepainter.paint(self, graph, axis)
1368 class baraxispainter(axistitlepainter):
1370 def __init__(self, innerticklength=None,
1371 outerticklength=None,
1372 tickattrs=(),
1373 baselineattrs=canvas.linecap.square,
1374 namedist="0.3 cm",
1375 nameattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1376 namedirection=None,
1377 namepos=0.5,
1378 namehequalize=0,
1379 namevequalize=1,
1380 **args):
1381 self.innerticklength_str = innerticklength
1382 self.outerticklength_str = outerticklength
1383 self.tickattrs = tickattrs
1384 self.baselineattrs = baselineattrs
1385 self.namedist_str = namedist
1386 self.nameattrs = nameattrs
1387 self.namedirection = namedirection
1388 self.namepos = namepos
1389 self.namehequalize = namehequalize
1390 self.namevequalize = namevequalize
1391 axistitlepainter.__init__(self, **args)
1393 def dolayout(self, graph, axis):
1394 axis.layoutdata._extent = 0
1395 if axis.multisubaxis:
1396 for name, subaxis in zip(axis.names, axis.subaxis):
1397 subaxis.vmin = axis.convert((name, 0))
1398 subaxis.vmax = axis.convert((name, 1))
1399 subaxis.baseaxis = axis
1400 subaxis._vtickpoint = lambda axis, v: axis.baseaxis._vtickpoint(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1401 subaxis.vtickdirection = lambda axis, v: axis.baseaxis.vtickdirection(axis.baseaxis, axis.vmin+v*(axis.vmax-axis.vmin))
1402 subaxis.vbaseline = None
1403 subaxis.dolayout(graph)
1404 if axis.layoutdata._extent < subaxis.layoutdata._extent:
1405 axis.layoutdata._extent = subaxis.layoutdata._extent
1406 axis.namepos = []
1407 for name in axis.names:
1408 v = axis.convert((name, self.namepos))
1409 x, y = axis._vtickpoint(axis, v)
1410 dx, dy = axis.vtickdirection(axis, v)
1411 axis.namepos.append((v, x, y, dx, dy))
1412 axis.nameboxes = []
1413 for (v, x, y, dx, dy), name in zip(axis.namepos, axis.names):
1414 nameattrs = helper.ensurelist(self.nameattrs)
1415 if self.namedirection is not None:
1416 nameattrs += [trafo.rotate(self.reldirection(self.namedirection, dx, dy))]
1417 if axis.texts.has_key(name):
1418 axis.nameboxes.append(textmodule._text(x, y, str(axis.texts[name]), *nameattrs))
1419 elif axis.texts.has_key(str(name)):
1420 axis.nameboxes.append(textmodule._text(x, y, str(axis.texts[str(name)]), *nameattrs))
1421 else:
1422 axis.nameboxes.append(textmodule._text(x, y, str(name), *nameattrs))
1423 labeldist = axis.layoutdata._extent + unit.topt(unit.length(self.namedist_str, default_type="v"))
1424 if len(axis.namepos) > 1:
1425 equaldirection = 1
1426 for namepos in axis.namepos[1:]:
1427 if namepos[3] != axis.namepos[0][3] or namepos[4] != axis.namepos[0][4]:
1428 equaldirection = 0
1429 else:
1430 equaldirection = 0
1431 if equaldirection and ((not axis.namepos[0][3] and self.namevequalize) or
1432 (not axis.namepos[0][4] and self.namehequalize)):
1433 box._linealignequal(axis.nameboxes, labeldist, axis.namepos[0][3], axis.namepos[0][4])
1434 else:
1435 for namebox, namepos in zip(axis.nameboxes, axis.namepos):
1436 namebox._linealign(labeldist, namepos[3], namepos[4])
1437 if self.innerticklength_str is not None:
1438 axis.innerticklength = unit.topt(unit.length(self.innerticklength_str, default_type="v"))
1439 else:
1440 if self.outerticklength_str is not None:
1441 axis.innerticklength = 0
1442 else:
1443 axis.innerticklength = None
1444 if self.outerticklength_str is not None:
1445 axis.outerticklength = unit.topt(unit.length(self.outerticklength_str, default_type="v"))
1446 else:
1447 if self.innerticklength_str is not None:
1448 axis.outerticklength = 0
1449 else:
1450 axis.outerticklength = None
1451 if axis.outerticklength is not None and self.tickattrs is not None:
1452 axis.layoutdata._extent += axis.outerticklength
1453 for (v, x, y, dx, dy), namebox in zip(axis.namepos, axis.nameboxes):
1454 newextent = namebox._extent(dx, dy) + labeldist
1455 if axis.layoutdata._extent < newextent:
1456 axis.layoutdata._extent = newextent
1457 axistitlepainter.dolayout(self, graph, axis)
1458 graph.mindbbox(*[namebox.bbox() for namebox in axis.nameboxes])
1460 def paint(self, graph, axis):
1461 if axis.subaxis is not None:
1462 if axis.multisubaxis:
1463 for subaxis in axis.subaxis:
1464 subaxis.dopaint(graph)
1465 if None not in (self.tickattrs, axis.innerticklength, axis.outerticklength):
1466 for pos in axis.relsizes:
1467 if pos == axis.relsizes[0]:
1468 pos -= axis.firstdist
1469 elif pos != axis.relsizes[-1]:
1470 pos -= 0.5 * axis.dist
1471 v = pos / axis.relsizes[-1]
1472 x, y = axis._vtickpoint(axis, v)
1473 dx, dy = axis.vtickdirection(axis, v)
1474 x1 = x - dx * axis.innerticklength
1475 y1 = y - dy * axis.innerticklength
1476 x2 = x + dx * axis.outerticklength
1477 y2 = y + dy * axis.outerticklength
1478 graph.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(self.tickattrs))
1479 if self.baselineattrs is not None:
1480 if axis.vbaseline is not None: # XXX: subbaselines (as for splitlines)
1481 graph.stroke(axis.vbaseline(axis), *helper.ensuresequence(self.baselineattrs))
1482 for namebox in axis.nameboxes:
1483 graph.insert(namebox)
1484 axistitlepainter.paint(self, graph, axis)
1488 ################################################################################
1489 # axes
1490 ################################################################################
1492 class PartitionError(Exception): pass
1494 class _axis:
1496 def __init__(self, min=None, max=None, reverse=0, divisor=1,
1497 datavmin=None, datavmax=None, tickvmin=0, tickvmax=1,
1498 title=None, suffix=None, painter=axispainter(), dense=None):
1499 if None not in (min, max) and min > max:
1500 min, max, reverse = max, min, not reverse
1501 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
1503 self.datamin = self.datamax = self.tickmin = self.tickmax = None
1504 if datavmin is None:
1505 if self.fixmin:
1506 self.datavmin = 0
1507 else:
1508 self.datavmin = 0.05
1509 else:
1510 self.datavmin = datavmin
1511 if datavmax is None:
1512 if self.fixmax:
1513 self.datavmax = 1
1514 else:
1515 self.datavmax = 0.95
1516 else:
1517 self.datavmax = datavmax
1518 self.tickvmin = tickvmin
1519 self.tickvmax = tickvmax
1521 self.divisor = divisor
1522 self.title = title
1523 self.suffix = suffix
1524 self.painter = painter
1525 self.dense = dense
1526 self.canconvert = 0
1527 self.__setinternalrange()
1529 def __setinternalrange(self, min=None, max=None):
1530 if not self.fixmin and min is not None and (self.min is None or min < self.min):
1531 self.min = min
1532 if not self.fixmax and max is not None and (self.max is None or max > self.max):
1533 self.max = max
1534 if None not in (self.min, self.max):
1535 min, max, vmin, vmax = self.min, self.max, 0, 1
1536 self.canconvert = 1
1537 self.setbasepoints(((min, vmin), (max, vmax)))
1538 if not self.fixmin:
1539 if self.datamin is not None and self.convert(self.datamin) < self.datavmin:
1540 min, vmin = self.datamin, self.datavmin
1541 self.setbasepoints(((min, vmin), (max, vmax)))
1542 if self.tickmin is not None and self.convert(self.tickmin) < self.tickvmin:
1543 min, vmin = self.tickmin, self.tickvmin
1544 self.setbasepoints(((min, vmin), (max, vmax)))
1545 if not self.fixmax:
1546 if self.datamax is not None and self.convert(self.datamax) > self.datavmax:
1547 max, vmax = self.datamax, self.datavmax
1548 self.setbasepoints(((min, vmin), (max, vmax)))
1549 if self.tickmax is not None and self.convert(self.tickmax) > self.tickvmax:
1550 max, vmax = self.tickmax, self.tickvmax
1551 self.setbasepoints(((min, vmin), (max, vmax)))
1552 if self.reverse:
1553 self.setbasepoints(((min, vmax), (max, vmin)))
1555 def __getinternalrange(self):
1556 return self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax
1558 def __forceinternalrange(self, range):
1559 self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax = range
1560 self.__setinternalrange()
1562 def setdatarange(self, min, max):
1563 self.datamin, self.datamax = min, max
1564 self.__setinternalrange(min, max)
1566 def settickrange(self, min, max):
1567 self.tickmin, self.tickmax = min, max
1568 self.__setinternalrange(min, max)
1570 def getdatarange(self):
1571 if self.canconvert:
1572 if self.reverse:
1573 return self.invert(1-self.datavmin), self.invert(1-self.datavmax)
1574 else:
1575 return self.invert(self.datavmin), self.invert(self.datavmax)
1577 def gettickrange(self):
1578 if self.canconvert:
1579 if self.reverse:
1580 return self.invert(1-self.tickvmin), self.invert(1-self.tickvmax)
1581 else:
1582 return self.invert(self.tickvmin), self.invert(self.tickvmax)
1584 def dolayout(self, graph):
1585 if self.dense is not None:
1586 dense = self.dense
1587 else:
1588 dense = graph.dense
1589 min, max = self.gettickrange()
1590 self.ticks = self.part.defaultpart(min/self.divisor, max/self.divisor, not self.fixmin, not self.fixmax)
1591 # lesspart and morepart can be called after defaultpart,
1592 # although some axes may share their autoparting ---
1593 # it works, because the axes are processed sequentially
1594 first = 1
1595 maxworse = 2
1596 worse = 0
1597 while worse < maxworse:
1598 newticks = self.part.lesspart()
1599 if newticks is not None:
1600 if first:
1601 bestrate = self.rate.ratepart(self, self.ticks, dense)
1602 variants = [[bestrate, self.ticks]]
1603 first = 0
1604 newrate = self.rate.ratepart(self, newticks, dense)
1605 variants.append([newrate, newticks])
1606 if newrate < bestrate:
1607 bestrate = newrate
1608 worse = 0
1609 else:
1610 worse += 1
1611 else:
1612 worse += 1
1613 worse = 0
1614 while worse < maxworse:
1615 newticks = self.part.morepart()
1616 if newticks is not None:
1617 if first:
1618 bestrate = self.rate.ratepart(self, self.ticks, dense)
1619 variants = [[bestrate, self.ticks]]
1620 first = 0
1621 newrate = self.rate.ratepart(self, newticks, dense)
1622 variants.append([newrate, newticks])
1623 if newrate < bestrate:
1624 bestrate = newrate
1625 worse = 0
1626 else:
1627 worse += 1
1628 else:
1629 worse += 1
1630 if not first:
1631 variants.sort()
1632 if self.painter is not None:
1633 i = 0
1634 bestrate = None
1635 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
1636 saverange = self.__getinternalrange()
1637 self.ticks = variants[i][1]
1638 if len(self.ticks):
1639 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
1640 self.layoutdata = layoutdata()
1641 self.painter.dolayout(graph, self)
1642 ratelayout = self.painter.ratelayout(graph, self, dense)
1643 if ratelayout is not None:
1644 variants[i][0] += ratelayout
1645 variants[i].append(self.layoutdata)
1646 else:
1647 variants[i][0] = None
1648 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
1649 bestrate = variants[i][0]
1650 self.__forceinternalrange(saverange)
1651 i += 1
1652 if bestrate is None:
1653 raise PartitionError("no valid axis partitioning found")
1654 variants = [variant for variant in variants[:i] if variant[0] is not None]
1655 variants.sort()
1656 self.ticks = variants[0][1]
1657 self.layoutdata = variants[0][2]
1658 else:
1659 for tick in self.ticks:
1660 tick.textbox = None
1661 self.layoutdata = layoutdata()
1662 self.layoutdata._extent = 0
1663 if len(self.ticks):
1664 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
1665 else:
1666 if len(self.ticks):
1667 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
1668 self.layoutdata = layoutdata()
1669 self.painter.dolayout(graph, self)
1670 graph.mindbbox(*[tick.textbox.bbox() for tick in self.ticks if tick.textbox is not None])
1672 def dopaint(self, graph):
1673 if self.painter is not None:
1674 self.painter.paint(graph, self)
1676 def createlinkaxis(self, **args):
1677 return linkaxis(self, **args)
1680 class linaxis(_axis, _linmap):
1682 def __init__(self, part=autolinpart(), rate=axisrater(), **args):
1683 _axis.__init__(self, **args)
1684 if self.fixmin and self.fixmax:
1685 self.relsize = self.max - self.min
1686 self.part = part
1687 self.rate = rate
1690 class logaxis(_axis, _logmap):
1692 def __init__(self, part=autologpart(), rate=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
1693 _axis.__init__(self, **args)
1694 if self.fixmin and self.fixmax:
1695 self.relsize = math.log(self.max) - math.log(self.min)
1696 self.part = part
1697 self.rate = rate
1700 class linkaxis:
1702 def __init__(self, linkedaxis, title=None, skipticklevel=None, skiplabellevel=0, painter=axispainter(zerolineattrs=None)):
1703 self.linkedaxis = linkedaxis
1704 while isinstance(self.linkedaxis, linkaxis):
1705 self.linkedaxis = self.linkedaxis.linkedaxis
1706 self.fixmin = self.linkedaxis.fixmin
1707 self.fixmax = self.linkedaxis.fixmax
1708 if self.fixmin:
1709 self.min = self.linkedaxis.min
1710 if self.fixmax:
1711 self.max = self.linkedaxis.max
1712 self.skipticklevel = skipticklevel
1713 self.skiplabellevel = skiplabellevel
1714 self.title = title
1715 self.painter = painter
1717 def ticks(self, ticks):
1718 result = []
1719 for _tick in ticks:
1720 ticklevel = _tick.ticklevel
1721 labellevel = _tick.labellevel
1722 if self.skipticklevel is not None and ticklevel >= self.skipticklevel:
1723 ticklevel = None
1724 if self.skiplabellevel is not None and labellevel >= self.skiplabellevel:
1725 labellevel = None
1726 if ticklevel is not None or labellevel is not None:
1727 result.append(tick(_tick.enum, _tick.denom, ticklevel, labellevel))
1728 return result
1729 # XXX: don't forget to calculate new text positions as soon as this is moved
1730 # outside of the paint method (when rating is moved into the axispainter)
1732 def getdatarange(self):
1733 return self.linkedaxis.getdatarange()
1735 def setdatarange(self, min, max):
1736 prevrange = self.linkedaxis.getdatarange()
1737 self.linkedaxis.setdatarange(min, max)
1738 if hasattr(self.linkedaxis, "ticks") and prevrange != self.linkedaxis.getdatarange():
1739 raise RuntimeError("linkaxis datarange setting performed while linked axis layout already fixed")
1741 def dolayout(self, graph):
1742 self.ticks = self.ticks(self.linkedaxis.ticks)
1743 self.convert = self.linkedaxis.convert
1744 self.divisor = self.linkedaxis.divisor
1745 self.suffix = self.linkedaxis.suffix
1746 self.layoutdata = layoutdata()
1747 self.painter.dolayout(graph, self)
1749 def dopaint(self, graph):
1750 self.painter.paint(graph, self)
1752 def createlinkaxis(self, **args):
1753 return linkaxis(self.linkedaxis)
1756 class splitaxis:
1758 def __init__(self, axislist, splitlist=0.5, splitdist=0.1, relsizesplitdist=1, title=None, painter=splitaxispainter()):
1759 self.title = title
1760 self.axislist = axislist
1761 self.painter = painter
1762 self.splitlist = list(helper.ensuresequence(splitlist))
1763 self.splitlist.sort()
1764 if len(self.axislist) != len(self.splitlist) + 1:
1765 for subaxis in self.axislist:
1766 if not isinstance(subaxis, linkaxis):
1767 raise ValueError("axislist and splitlist lengths do not fit together")
1768 for subaxis in self.axislist:
1769 if isinstance(subaxis, linkaxis):
1770 subaxis.vmin = subaxis.linkedaxis.vmin
1771 subaxis.vminover = subaxis.linkedaxis.vminover
1772 subaxis.vmax = subaxis.linkedaxis.vmax
1773 subaxis.vmaxover = subaxis.linkedaxis.vmaxover
1774 else:
1775 subaxis.vmin = None
1776 subaxis.vmax = None
1777 self.axislist[0].vmin = 0
1778 self.axislist[0].vminover = None
1779 self.axislist[-1].vmax = 1
1780 self.axislist[-1].vmaxover = None
1781 for i in xrange(len(self.splitlist)):
1782 if self.splitlist[i] is not None:
1783 self.axislist[i].vmax = self.splitlist[i] - 0.5*splitdist
1784 self.axislist[i].vmaxover = self.splitlist[i]
1785 self.axislist[i+1].vmin = self.splitlist[i] + 0.5*splitdist
1786 self.axislist[i+1].vminover = self.splitlist[i]
1787 i = 0
1788 while i < len(self.axislist):
1789 if self.axislist[i].vmax is None:
1790 j = relsize = relsize2 = 0
1791 while self.axislist[i + j].vmax is None:
1792 relsize += self.axislist[i + j].relsize + relsizesplitdist
1793 j += 1
1794 relsize += self.axislist[i + j].relsize
1795 vleft = self.axislist[i].vmin
1796 vright = self.axislist[i + j].vmax
1797 for k in range(i, i + j):
1798 relsize2 += self.axislist[k].relsize
1799 self.axislist[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
1800 relsize2 += 0.5 * relsizesplitdist
1801 self.axislist[k].vmaxover = self.axislist[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
1802 relsize2 += 0.5 * relsizesplitdist
1803 self.axislist[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
1804 if i == 0 and i + j + 1 == len(self.axislist):
1805 self.relsize = relsize
1806 i += j + 1
1807 else:
1808 i += 1
1810 self.fixmin = self.axislist[0].fixmin
1811 if self.fixmin:
1812 self.min = self.axislist[0].min
1813 self.fixmax = self.axislist[-1].fixmax
1814 if self.fixmax:
1815 self.max = self.axislist[-1].max
1816 self.divisor = 1
1817 self.suffix = ""
1819 def getdatarange(self):
1820 min = self.axislist[0].getdatarange()
1821 max = self.axislist[-1].getdatarange()
1822 try:
1823 return min[0], max[1]
1824 except TypeError:
1825 return None
1827 def setdatarange(self, min, max):
1828 self.axislist[0].setdatarange(min, None)
1829 self.axislist[-1].setdatarange(None, max)
1831 def gettickrange(self):
1832 min = self.axislist[0].gettickrange()
1833 max = self.axislist[-1].gettickrange()
1834 try:
1835 return min[0], max[1]
1836 except TypeError:
1837 return None
1839 def settickrange(self, min, max):
1840 self.axislist[0].settickrange(min, None)
1841 self.axislist[-1].settickrange(None, max)
1843 def convert(self, value):
1844 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
1845 if value < self.axislist[0].max:
1846 return self.axislist[0].vmin + self.axislist[0].convert(value)*(self.axislist[0].vmax-self.axislist[0].vmin)
1847 for axis in self.axislist[1:-1]:
1848 if value > axis.min and value < axis.max:
1849 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
1850 if value > self.axislist[-1].min:
1851 return self.axislist[-1].vmin + self.axislist[-1].convert(value)*(self.axislist[-1].vmax-self.axislist[-1].vmin)
1852 raise ValueError("value couldn't be assigned to a split region")
1854 def dolayout(self, graph):
1855 self.layoutdata = layoutdata()
1856 self.painter.dolayout(graph, self)
1858 def dopaint(self, graph):
1859 self.painter.paint(graph, self)
1861 def createlinkaxis(self, painter=None, *args):
1862 if not len(args):
1863 return splitaxis([x.createlinkaxis() for x in self.axislist], splitlist=None)
1864 if len(args) != len(self.axislist):
1865 raise IndexError("length of the argument list doesn't fit to split number")
1866 if painter is None:
1867 painter = self.painter
1868 return splitaxis([x.createlinkaxis(**arg) for x, arg in zip(self.axislist, args)], painter=painter)
1871 class baraxis:
1873 def __init__(self, subaxis=None, multisubaxis=0, title=None, dist=0.5, firstdist=None, lastdist=None, names=None, texts={}, painter=baraxispainter()):
1874 self.dist = dist
1875 if firstdist is not None:
1876 self.firstdist = firstdist
1877 else:
1878 self.firstdist = 0.5 * dist
1879 if lastdist is not None:
1880 self.lastdist = lastdist
1881 else:
1882 self.lastdist = 0.5 * dist
1883 self.relsizes = None
1884 self.fixnames = 0
1885 self.names = []
1886 for name in helper.ensuresequence(names):
1887 self.setname(name)
1888 self.fixnames = names is not None
1889 self.multisubaxis = multisubaxis
1890 if self.multisubaxis:
1891 self.createsubaxis = subaxis
1892 self.subaxis = [self.createsubaxis.createsubaxis() for name in self.names]
1893 else:
1894 self.subaxis = subaxis
1895 self.title = title
1896 self.fixnames = 0
1897 self.texts = texts
1898 self.painter = painter
1900 def getdatarange(self):
1901 return None
1903 def setname(self, name, *subnames):
1904 # setting self.relsizes to None forces later recalculation
1905 if not self.fixnames:
1906 if name not in self.names:
1907 self.relsizes = None
1908 self.names.append(name)
1909 if self.multisubaxis:
1910 self.subaxis.append(self.createsubaxis.createsubaxis())
1911 if (not self.fixnames or name in self.names) and len(subnames):
1912 if self.multisubaxis:
1913 if self.subaxis[self.names.index(name)].setname(*subnames):
1914 self.relsizes = None
1915 else:
1916 if self.subaxis.setname(*subnames):
1917 self.relsizes = None
1918 return self.relsizes is not None
1920 def updaterelsizes(self):
1921 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
1922 self.relsizes[-1] += self.lastdist - self.dist
1923 if self.multisubaxis:
1924 subrelsize = 0
1925 for i in range(1, len(self.relsizes)):
1926 self.subaxis[i-1].updaterelsizes()
1927 subrelsize += self.subaxis[i-1].relsizes[-1]
1928 self.relsizes[i] += subrelsize
1929 else:
1930 if self.subaxis is None:
1931 subrelsize = 1
1932 else:
1933 self.subaxis.updaterelsizes()
1934 subrelsize = self.subaxis.relsizes[-1]
1935 for i in range(1, len(self.relsizes)):
1936 self.relsizes[i] += i * subrelsize
1938 def convert(self, value):
1939 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
1940 if not self.relsizes:
1941 self.updaterelsizes()
1942 pos = self.names.index(value[0])
1943 if len(value) == 2:
1944 if self.subaxis is None:
1945 subvalue = value[1]
1946 else:
1947 if self.multisubaxis:
1948 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
1949 else:
1950 subvalue = value[1] * self.subaxis.relsizes[-1]
1951 else:
1952 if self.multisubaxis:
1953 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
1954 else:
1955 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
1956 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
1958 def dolayout(self, graph):
1959 self.layoutdata = layoutdata()
1960 self.painter.dolayout(graph, self)
1962 def dopaint(self, graph):
1963 self.painter.paint(graph, self)
1965 def createlinkaxis(self, **args):
1966 if self.subaxis is not None:
1967 if self.multisubaxis:
1968 subaxis = [subaxis.createlinkaxis() for subaxis in self.subaxis]
1969 else:
1970 subaxis = self.subaxis.createlinkaxis()
1971 else:
1972 subaxis = None
1973 return baraxis(subaxis=subaxis, dist=self.dist, firstdist=self.firstdist, lastdist=self.lastdist, **args)
1975 createsubaxis = createlinkaxis
1978 ################################################################################
1979 # graph key
1980 ################################################################################
1983 # g = graph.graphxy(key=graph.key())
1984 # g.addkey(graph.key(), ...)
1987 class key:
1989 def __init__(self, dist="0.2 cm", pos = "tr", hinside = 1, vinside = 1, hdist="0.6 cm", vdist="0.4 cm",
1990 symbolwidth="0.5 cm", symbolheight="0.25 cm", symbolspace="0.2 cm",
1991 textattrs=textmodule.vshift.mathaxis):
1992 self.dist_str = dist
1993 self.pos = pos
1994 self.hinside = hinside
1995 self.vinside = vinside
1996 self.hdist_str = hdist
1997 self.vdist_str = vdist
1998 self.symbolwidth_str = symbolwidth
1999 self.symbolheight_str = symbolheight
2000 self.symbolspace_str = symbolspace
2001 self.textattrs = textattrs
2002 self.plotinfos = None
2003 if self.pos in ("tr", "rt"):
2004 self.right = 1
2005 self.top = 1
2006 elif self.pos in ("br", "rb"):
2007 self.right = 1
2008 self.top = 0
2009 elif self.pos in ("tl", "lt"):
2010 self.right = 0
2011 self.top = 1
2012 elif self.pos in ("bl", "lb"):
2013 self.right = 0
2014 self.top = 0
2015 else:
2016 raise RuntimeError("invalid pos attribute")
2018 def setplotinfos(self, *plotinfos):
2019 """set the plotinfos to be used in the key
2020 - call it exactly once"""
2021 if self.plotinfos is not None:
2022 raise RuntimeError("setplotinfo is called multiple times")
2023 self.plotinfos = plotinfos
2025 def dolayout(self, graph):
2026 """creates the layout of the key"""
2027 self._dist = unit.topt(unit.length(self.dist_str, default_type="v"))
2028 self._hdist = unit.topt(unit.length(self.hdist_str, default_type="v"))
2029 self._vdist = unit.topt(unit.length(self.vdist_str, default_type="v"))
2030 self._symbolwidth = unit.topt(unit.length(self.symbolwidth_str, default_type="v"))
2031 self._symbolheight = unit.topt(unit.length(self.symbolheight_str, default_type="v"))
2032 self._symbolspace = unit.topt(unit.length(self.symbolspace_str, default_type="v"))
2033 self.titles = []
2034 for plotinfo in self.plotinfos:
2035 self.titles.append(graph.texrunner._text(0, 0, plotinfo.data.title, *helper.ensuresequence(self.textattrs)))
2036 box._tile(self.titles, self._dist, 0, -1)
2037 box._linealignequal(self.titles, self._symbolwidth + self._symbolspace, 1, 0)
2039 def bbox(self):
2040 """return a bbox for the key
2041 method should be called after dolayout"""
2042 result = self.titles[0].bbox()
2043 for title in self.titles[1:]:
2044 result = result + title.bbox() + bbox.bbox(0, title.center[1] - 0.5 * self._symbolheight,
2045 0, title.center[1] + 0.5 * self._symbolheight)
2046 return result
2048 def paint(self, c, x, y):
2049 """paint the graph key into a canvas c at the position x and y (in postscript points)
2050 - method should be called after dolayout
2051 - the x, y alignment might be calculated by the graph using:
2052 - the bbox of the key as returned by the keys bbox method
2053 - the attributes _hdist, _vdist, hinside, and vinside of the key
2054 - the dimension and geometry of the graph"""
2055 sc = c.insert(canvas.canvas(trafo._translate(x, y)))
2056 for plotinfo, title in zip(self.plotinfos, self.titles):
2057 plotinfo.style.key(sc, 0, -0.5 * self._symbolheight + title.center[1],
2058 self._symbolwidth, self._symbolheight)
2059 sc.insert(title)
2062 ################################################################################
2063 # graph
2064 ################################################################################
2067 class plotinfo:
2069 def __init__(self, data, style):
2070 self.data = data
2071 self.style = style
2074 class graphxy(canvas.canvas):
2076 Names = "x", "y"
2078 def clipcanvas(self):
2079 return self.insert(canvas.canvas(canvas.clip(path._rect(self._xpos, self._ypos, self._width, self._height))))
2081 def plot(self, data, style=None):
2082 if self.haslayout:
2083 raise RuntimeError("layout setup was already performed")
2084 if style is None:
2085 if helper.issequence(data):
2086 raise RuntimeError("list plot needs an explicit style")
2087 if self.defaultstyle.has_key(data.defaultstyle):
2088 style = self.defaultstyle[data.defaultstyle].iterate()
2089 else:
2090 style = data.defaultstyle()
2091 self.defaultstyle[data.defaultstyle] = style
2092 plotinfos = []
2093 first = 1
2094 for d in helper.ensuresequence(data):
2095 if not first:
2096 style = style.iterate()
2097 first = 0
2098 if d is not None:
2099 d.setstyle(self, style)
2100 plotinfos.append(plotinfo(d, style))
2101 self.plotinfos.extend(plotinfos)
2102 if helper.issequence(data):
2103 return plotinfos
2104 return plotinfos[0]
2106 def addkey(self, key, *plotinfos):
2107 if self.haslayout:
2108 raise RuntimeError("layout setup was already performed")
2109 self.addkeys.append((key, plotinfos))
2111 def _vxtickpoint(self, axis, v):
2112 return (self._xpos+v*self._width, axis.axispos)
2114 def _vytickpoint(self, axis, v):
2115 return (axis.axispos, self._ypos+v*self._height)
2117 def vtickdirection(self, axis, v):
2118 return axis.fixtickdirection
2120 def _pos(self, x, y, xaxis=None, yaxis=None):
2121 if xaxis is None: xaxis = self.axes["x"]
2122 if yaxis is None: yaxis = self.axes["y"]
2123 return self._xpos+xaxis.convert(x)*self._width, self._ypos+yaxis.convert(y)*self._height
2125 def pos(self, x, y, xaxis=None, yaxis=None):
2126 if xaxis is None: xaxis = self.axes["x"]
2127 if yaxis is None: yaxis = self.axes["y"]
2128 return self.xpos+xaxis.convert(x)*self.width, self.ypos+yaxis.convert(y)*self.height
2130 def _vpos(self, vx, vy):
2131 return self._xpos+vx*self._width, self._ypos+vy*self._height
2133 def vpos(self, vx, vy):
2134 return self.xpos+vx*self.width, self.ypos+vy*self.height
2136 def xbaseline(self, axis, x1, x2, shift=0, xaxis=None):
2137 if xaxis is None: xaxis = self.axes["x"]
2138 v1, v2 = xaxis.convert(x1), xaxis.convert(x2)
2139 return path._line(self._xpos+v1*self._width, axis.axispos+shift,
2140 self._xpos+v2*self._width, axis.axispos+shift)
2142 def ybaseline(self, axis, y1, y2, shift=0, yaxis=None):
2143 if yaxis is None: yaxis = self.axes["y"]
2144 v1, v2 = yaxis.convert(y1), yaxis.convert(y2)
2145 return path._line(axis.axispos+shift, self._ypos+v1*self._height,
2146 axis.axispos+shift, self._ypos+v2*self._height)
2148 def vxbaseline(self, axis, v1=None, v2=None, shift=0):
2149 if v1 is None: v1 = 0
2150 if v2 is None: v2 = 1
2151 return path._line(self._xpos+v1*self._width, axis.axispos+shift,
2152 self._xpos+v2*self._width, axis.axispos+shift)
2154 def vybaseline(self, axis, v1=None, v2=None, shift=0):
2155 if v1 is None: v1 = 0
2156 if v2 is None: v2 = 1
2157 return path._line(axis.axispos+shift, self._ypos+v1*self._height,
2158 axis.axispos+shift, self._ypos+v2*self._height)
2160 def xgridpath(self, x, xaxis=None):
2161 if xaxis is None: xaxis = self.axes["x"]
2162 v = xaxis.convert(x)
2163 return path._line(self._xpos+v*self._width, self._ypos,
2164 self._xpos+v*self._width, self._ypos+self._height)
2166 def ygridpath(self, y, yaxis=None):
2167 if yaxis is None: yaxis = self.axes["y"]
2168 v = yaxis.convert(y)
2169 return path._line(self._xpos, self._ypos+v*self._height,
2170 self._xpos+self._width, self._ypos+v*self._height)
2172 def vxgridpath(self, v):
2173 return path._line(self._xpos+v*self._width, self._ypos,
2174 self._xpos+v*self._width, self._ypos+self._height)
2176 def vygridpath(self, v):
2177 return path._line(self._xpos, self._ypos+v*self._height,
2178 self._xpos+self._width, self._ypos+v*self._height)
2180 def _addpos(self, x, y, dx, dy):
2181 return x+dx, y+dy
2183 def _connect(self, x1, y1, x2, y2):
2184 return path._lineto(x2, y2)
2186 def keynum(self, key):
2187 try:
2188 while key[0] in string.letters:
2189 key = key[1:]
2190 return int(key)
2191 except IndexError:
2192 return 1
2194 def gatherranges(self):
2195 ranges = {}
2196 for plotinfo in self.plotinfos:
2197 pdranges = plotinfo.data.getranges()
2198 if pdranges is not None:
2199 for key in pdranges.keys():
2200 if key not in ranges.keys():
2201 ranges[key] = pdranges[key]
2202 else:
2203 ranges[key] = (min(ranges[key][0], pdranges[key][0]),
2204 max(ranges[key][1], pdranges[key][1]))
2205 # known ranges are also set as ranges for the axes
2206 for key, axis in self.axes.items():
2207 if key in ranges.keys():
2208 axis.setdatarange(*ranges[key])
2209 ranges[key] = axis.getdatarange()
2210 if ranges[key] is None:
2211 del ranges[key]
2212 return ranges
2214 def removedomethod(self, method):
2215 hadmethod = 0
2216 while 1:
2217 try:
2218 self.domethods.remove(method)
2219 hadmethod = 1
2220 except ValueError:
2221 return hadmethod
2223 def dolayout(self):
2224 if not self.removedomethod(self.dolayout): return
2225 self.haslayout = 1
2226 # create list of ranges
2227 # 1. gather ranges
2228 ranges = self.gatherranges()
2229 # 2. calculate additional ranges out of known ranges
2230 for plotinfo in self.plotinfos:
2231 plotinfo.data.setranges(ranges)
2232 # 3. gather ranges again
2233 self.gatherranges()
2235 # do the layout for all axes
2236 axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
2237 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
2238 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
2239 self._xaxisextents = [0, 0]
2240 self._yaxisextents = [0, 0]
2241 needxaxisdist = [0, 0]
2242 needyaxisdist = [0, 0]
2243 items = list(self.axes.items())
2244 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
2245 for key, axis in items:
2246 num = self.keynum(key)
2247 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
2248 num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
2249 if XPattern.match(key):
2250 if needxaxisdist[num2]:
2251 self._xaxisextents[num2] += axesdist
2252 axis.axispos = self._ypos+num2*self._height + num3*self._xaxisextents[num2]
2253 axis._vtickpoint = self._vxtickpoint
2254 axis.fixtickdirection = (0, num3)
2255 axis.vgridpath = self.vxgridpath
2256 axis.vbaseline = self.vxbaseline
2257 axis.gridpath = self.xgridpath
2258 axis.baseline = self.xbaseline
2259 elif YPattern.match(key):
2260 if needyaxisdist[num2]:
2261 self._yaxisextents[num2] += axesdist
2262 axis.axispos = self._xpos+num2*self._width + num3*self._yaxisextents[num2]
2263 axis._vtickpoint = self._vytickpoint
2264 axis.fixtickdirection = (num3, 0)
2265 axis.vgridpath = self.vygridpath
2266 axis.vbaseline = self.vybaseline
2267 axis.gridpath = self.ygridpath
2268 axis.baseline = self.ybaseline
2269 else:
2270 raise ValueError("Axis key '%s' not allowed" % key)
2271 axis.vtickdirection = self.vtickdirection
2272 axis.dolayout(self)
2273 if XPattern.match(key):
2274 self._xaxisextents[num2] += axis.layoutdata._extent
2275 needxaxisdist[num2] = 1
2276 if YPattern.match(key):
2277 self._yaxisextents[num2] += axis.layoutdata._extent
2278 needyaxisdist[num2] = 1
2280 def dobackground(self):
2281 self.dolayout()
2282 if not self.removedomethod(self.dobackground): return
2283 if self.backgroundattrs is not None:
2284 self.draw(path._rect(self._xpos, self._ypos, self._width, self._height),
2285 *helper.ensuresequence(self.backgroundattrs))
2287 def doaxes(self):
2288 self.dolayout()
2289 if not self.removedomethod(self.doaxes): return
2290 for axis in self.axes.values():
2291 axis.dopaint(self)
2293 def dodata(self):
2294 self.dolayout()
2295 if not self.removedomethod(self.dodata): return
2296 for plotinfo in self.plotinfos:
2297 plotinfo.data.draw(self)
2299 def _dokey(self, key, *plotinfos):
2300 key.setplotinfos(*plotinfos)
2301 key.dolayout(self)
2302 bbox = key.bbox()
2303 if key.right:
2304 if key.hinside:
2305 x = self._xpos + self._width - bbox.urx - key._hdist
2306 else:
2307 x = self._xpos + self._width - bbox.llx + key._hdist
2308 else:
2309 if key.hinside:
2310 x = self._xpos - bbox.llx + key._hdist
2311 else:
2312 x = self._xpos - bbox.urx - key._hdist
2313 if key.top:
2314 if key.vinside:
2315 y = self._ypos + self._height - bbox.ury - key._vdist
2316 else:
2317 y = self._ypos + self._height - bbox.lly + key._vdist
2318 else:
2319 if key.vinside:
2320 y = self._ypos - bbox.lly + key._vdist
2321 else:
2322 y = self._ypos - bbox.ury - key._vdist
2323 self.mindbbox(bbox.transformed(trafo._translate(x, y)))
2324 key.paint(self, x, y)
2326 def dokey(self):
2327 self.dolayout()
2328 if not self.removedomethod(self.dokey): return
2329 if self.key is not None:
2330 self._dokey(self.key, *self.plotinfos)
2331 for key, plotinfos in self.addkeys:
2332 self._dokey(key, *plotinfos)
2334 def finish(self):
2335 while len(self.domethods):
2336 self.domethods[0]()
2338 def initwidthheight(self, width, height, ratio):
2339 if (width is not None) and (height is None):
2340 self.width = unit.length(width)
2341 self.height = (1.0/ratio) * self.width
2342 elif (height is not None) and (width is None):
2343 self.height = unit.length(height)
2344 self.width = ratio * self.height
2345 else:
2346 self.width = unit.length(width)
2347 self.height = unit.length(height)
2348 self._width = unit.topt(self.width)
2349 self._height = unit.topt(self.height)
2350 if self._width <= 0: raise ValueError("width <= 0")
2351 if self._height <= 0: raise ValueError("height <= 0")
2353 def initaxes(self, axes, addlinkaxes=0):
2354 for key in self.Names:
2355 if not axes.has_key(key):
2356 axes[key] = linaxis()
2357 elif axes[key] is None:
2358 del axes[key]
2359 if addlinkaxes:
2360 if not axes.has_key(key + "2") and axes.has_key(key):
2361 axes[key + "2"] = axes[key].createlinkaxis()
2362 elif axes[key + "2"] is None:
2363 del axes[key + "2"]
2364 self.axes = axes
2366 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
2367 key=None, backgroundattrs=None, dense=1, axesdist="0.8 cm", **axes):
2368 canvas.canvas.__init__(self)
2369 self.xpos = unit.length(xpos)
2370 self.ypos = unit.length(ypos)
2371 self._xpos = unit.topt(self.xpos)
2372 self._ypos = unit.topt(self.ypos)
2373 self.initwidthheight(width, height, ratio)
2374 self.initaxes(axes, 1)
2375 self.key = key
2376 self.backgroundattrs = backgroundattrs
2377 self.dense = dense
2378 self.axesdist_str = axesdist
2379 self.plotinfos = []
2380 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata, self.dokey]
2381 self.haslayout = 0
2382 self.defaultstyle = {}
2383 self.addkeys = []
2384 self.mindbboxes = []
2386 def mindbbox(self, *boxes):
2387 self.mindbboxes.extend(boxes)
2389 def bbox(self):
2390 self.finish()
2391 result = bbox.bbox(self._xpos - self._yaxisextents[0],
2392 self._ypos - self._xaxisextents[0],
2393 self._xpos + self._width + self._yaxisextents[1],
2394 self._ypos + self._height + self._xaxisextents[1])
2395 for box in self.mindbboxes:
2396 result = result + box
2397 return result
2399 def write(self, file):
2400 self.finish()
2401 canvas.canvas.write(self, file)
2405 # some thoughts, but deferred right now
2407 # class graphxyz(graphxy):
2409 # Names = "x", "y", "z"
2411 # def _vxtickpoint(self, axis, v):
2412 # return self._vpos(v, axis.vypos, axis.vzpos)
2414 # def _vytickpoint(self, axis, v):
2415 # return self._vpos(axis.vxpos, v, axis.vzpos)
2417 # def _vztickpoint(self, axis, v):
2418 # return self._vpos(axis.vxpos, axis.vypos, v)
2420 # def vxtickdirection(self, axis, v):
2421 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
2422 # x2, y2 = self._vpos(v, 0.5, 0)
2423 # dx, dy = x1 - x2, y1 - y2
2424 # norm = math.sqrt(dx*dx + dy*dy)
2425 # return dx/norm, dy/norm
2427 # def vytickdirection(self, axis, v):
2428 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
2429 # x2, y2 = self._vpos(0.5, v, 0)
2430 # dx, dy = x1 - x2, y1 - y2
2431 # norm = math.sqrt(dx*dx + dy*dy)
2432 # return dx/norm, dy/norm
2434 # def vztickdirection(self, axis, v):
2435 # return -1, 0
2436 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
2437 # x2, y2 = self._vpos(0.5, 0.5, v)
2438 # dx, dy = x1 - x2, y1 - y2
2439 # norm = math.sqrt(dx*dx + dy*dy)
2440 # return dx/norm, dy/norm
2442 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
2443 # if xaxis is None: xaxis = self.axes["x"]
2444 # if yaxis is None: yaxis = self.axes["y"]
2445 # if zaxis is None: zaxis = self.axes["z"]
2446 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
2448 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
2449 # if xaxis is None: xaxis = self.axes["x"]
2450 # if yaxis is None: yaxis = self.axes["y"]
2451 # if zaxis is None: zaxis = self.axes["z"]
2452 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
2454 # def _vpos(self, vx, vy, vz):
2455 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
2456 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
2457 # + self.a[2]*self.b[0]*(y-self.eye[1])
2458 # + self.a[1]*self.b[2]*(x-self.eye[0])
2459 # - self.a[2]*self.b[1]*(x-self.eye[0])
2460 # - self.a[0]*self.b[2]*(y-self.eye[1])
2461 # - self.a[1]*self.b[0]*(z-self.eye[2]))
2462 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
2463 # + self.eye[2]*self.b[0]*(y-self.eye[1])
2464 # + self.eye[1]*self.b[2]*(x-self.eye[0])
2465 # - self.eye[2]*self.b[1]*(x-self.eye[0])
2466 # - self.eye[0]*self.b[2]*(y-self.eye[1])
2467 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
2468 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
2469 # + self.a[2]*self.eye[0]*(y-self.eye[1])
2470 # + self.a[1]*self.eye[2]*(x-self.eye[0])
2471 # - self.a[2]*self.eye[1]*(x-self.eye[0])
2472 # - self.a[0]*self.eye[2]*(y-self.eye[1])
2473 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
2474 # return da/d0 + self._xpos, db/d0 + self._ypos
2476 # def vpos(self, vx, vy, vz):
2477 # tx, ty = self._vpos(vx, vy, vz)
2478 # return unit.t_pt(tx), unit.t_pt(ty)
2480 # def xbaseline(self, axis, x1, x2, shift=0, xaxis=None):
2481 # if xaxis is None: xaxis = self.axes["x"]
2482 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2), shift)
2484 # def ybaseline(self, axis, y1, y2, shift=0, yaxis=None):
2485 # if yaxis is None: yaxis = self.axes["y"]
2486 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2), shift)
2488 # def zbaseline(self, axis, z1, z2, shift=0, zaxis=None):
2489 # if zaxis is None: zaxis = self.axes["z"]
2490 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2), shift)
2492 # def vxbaseline(self, axis, v1, v2, shift=0):
2493 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
2494 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
2495 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
2496 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
2498 # def vybaseline(self, axis, v1, v2, shift=0):
2499 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
2500 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
2501 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
2502 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
2504 # def vzbaseline(self, axis, v1, v2, shift=0):
2505 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
2506 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
2507 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
2508 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
2510 # def xgridpath(self, x, xaxis=None):
2511 # assert 0
2512 # if xaxis is None: xaxis = self.axes["x"]
2513 # v = xaxis.convert(x)
2514 # return path._line(self._xpos+v*self._width, self._ypos,
2515 # self._xpos+v*self._width, self._ypos+self._height)
2517 # def ygridpath(self, y, yaxis=None):
2518 # assert 0
2519 # if yaxis is None: yaxis = self.axes["y"]
2520 # v = yaxis.convert(y)
2521 # return path._line(self._xpos, self._ypos+v*self._height,
2522 # self._xpos+self._width, self._ypos+v*self._height)
2524 # def zgridpath(self, z, zaxis=None):
2525 # assert 0
2526 # if zaxis is None: zaxis = self.axes["z"]
2527 # v = zaxis.convert(z)
2528 # return path._line(self._xpos, self._zpos+v*self._height,
2529 # self._xpos+self._width, self._zpos+v*self._height)
2531 # def vxgridpath(self, v):
2532 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
2533 # path._lineto(*self._vpos(v, 0, 1)),
2534 # path._lineto(*self._vpos(v, 1, 1)),
2535 # path._lineto(*self._vpos(v, 1, 0)),
2536 # path.closepath())
2538 # def vygridpath(self, v):
2539 # return path.path(path._moveto(*self._vpos(0, v, 0)),
2540 # path._lineto(*self._vpos(0, v, 1)),
2541 # path._lineto(*self._vpos(1, v, 1)),
2542 # path._lineto(*self._vpos(1, v, 0)),
2543 # path.closepath())
2545 # def vzgridpath(self, v):
2546 # return path.path(path._moveto(*self._vpos(0, 0, v)),
2547 # path._lineto(*self._vpos(0, 1, v)),
2548 # path._lineto(*self._vpos(1, 1, v)),
2549 # path._lineto(*self._vpos(1, 0, v)),
2550 # path.closepath())
2552 # def _addpos(self, x, y, dx, dy):
2553 # assert 0
2554 # return x+dx, y+dy
2556 # def _connect(self, x1, y1, x2, y2):
2557 # assert 0
2558 # return path._lineto(x2, y2)
2560 # def doaxes(self):
2561 # self.dolayout()
2562 # if not self.removedomethod(self.doaxes): return
2563 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
2564 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
2565 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
2566 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[2])
2567 # items = list(self.axes.items())
2568 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
2569 # for key, axis in items:
2570 # num = self.keynum(key)
2571 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
2572 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
2573 # if XPattern.match(key):
2574 # axis.vypos = 0
2575 # axis.vzpos = 0
2576 # axis._vtickpoint = self._vxtickpoint
2577 # axis.vgridpath = self.vxgridpath
2578 # axis.vbaseline = self.vxbaseline
2579 # axis.vtickdirection = self.vxtickdirection
2580 # elif YPattern.match(key):
2581 # axis.vxpos = 0
2582 # axis.vzpos = 0
2583 # axis._vtickpoint = self._vytickpoint
2584 # axis.vgridpath = self.vygridpath
2585 # axis.vbaseline = self.vybaseline
2586 # axis.vtickdirection = self.vytickdirection
2587 # elif ZPattern.match(key):
2588 # axis.vxpos = 0
2589 # axis.vypos = 0
2590 # axis._vtickpoint = self._vztickpoint
2591 # axis.vgridpath = self.vzgridpath
2592 # axis.vbaseline = self.vzbaseline
2593 # axis.vtickdirection = self.vztickdirection
2594 # else:
2595 # raise ValueError("Axis key '%s' not allowed" % key)
2596 # if axis.painter is not None:
2597 # axis.dopaint(self)
2598 # # if XPattern.match(key):
2599 # # self._xaxisextents[num2] += axis._extent
2600 # # needxaxisdist[num2] = 1
2601 # # if YPattern.match(key):
2602 # # self._yaxisextents[num2] += axis._extent
2603 # # needyaxisdist[num2] = 1
2605 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
2606 # phi=30, theta=30, distance=1,
2607 # backgroundattrs=None, axesdist="0.8 cm", **axes):
2608 # canvas.canvas.__init__(self)
2609 # self.tex = tex
2610 # self.xpos = xpos
2611 # self.ypos = ypos
2612 # self._xpos = unit.topt(xpos)
2613 # self._ypos = unit.topt(ypos)
2614 # self._width = unit.topt(width)
2615 # self._height = unit.topt(height)
2616 # self._depth = unit.topt(depth)
2617 # self.width = width
2618 # self.height = height
2619 # self.depth = depth
2620 # if self._width <= 0: raise ValueError("width < 0")
2621 # if self._height <= 0: raise ValueError("height < 0")
2622 # if self._depth <= 0: raise ValueError("height < 0")
2623 # self._distance = distance*math.sqrt(self._width*self._width+
2624 # self._height*self._height+
2625 # self._depth*self._depth)
2626 # phi *= -math.pi/180
2627 # theta *= math.pi/180
2628 # self.a = (-math.sin(phi), math.cos(phi), 0)
2629 # self.b = (-math.cos(phi)*math.sin(theta),
2630 # -math.sin(phi)*math.sin(theta),
2631 # math.cos(theta))
2632 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
2633 # self._distance*math.sin(phi)*math.cos(theta),
2634 # self._distance*math.sin(theta))
2635 # self.initaxes(axes)
2636 # self.axesdist_str = axesdist
2637 # self.backgroundattrs = backgroundattrs
2639 # self.data = []
2640 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
2641 # self.haslayout = 0
2642 # self.defaultstyle = {}
2644 # def bbox(self):
2645 # self.finish()
2646 # return bbox.bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
2649 ################################################################################
2650 # attr changers
2651 ################################################################################
2654 #class _Ichangeattr:
2655 # """attribute changer
2656 # is an iterator for attributes where an attribute
2657 # is not refered by just a number (like for a sequence),
2658 # but also by the number of attributes requested
2659 # by calls of the next method (like for an color palette)
2660 # (you should ensure to call all needed next before the attr)
2662 # the attribute itself is implemented by overloading the _attr method"""
2664 # def attr(self):
2665 # "get an attribute"
2667 # def next(self):
2668 # "get an attribute changer for the next attribute"
2671 class _changeattr: pass
2674 class changeattr(_changeattr):
2676 def __init__(self):
2677 self.counter = 1
2679 def getattr(self):
2680 return self.attr(0)
2682 def iterate(self):
2683 newindex = self.counter
2684 self.counter += 1
2685 return refattr(self, newindex)
2688 class refattr(_changeattr):
2690 def __init__(self, ref, index):
2691 self.ref = ref
2692 self.index = index
2694 def getattr(self):
2695 return self.ref.attr(self.index)
2697 def iterate(self):
2698 return self.ref.iterate()
2701 # helper routines for a using attrs
2703 def _getattr(attr):
2704 """get attr out of a attr/changeattr"""
2705 if isinstance(attr, _changeattr):
2706 return attr.getattr()
2707 return attr
2710 def _getattrs(attrs):
2711 """get attrs out of a sequence of attr/changeattr"""
2712 if attrs is not None:
2713 result = []
2714 for attr in helper.ensuresequence(attrs):
2715 if isinstance(attr, _changeattr):
2716 result.append(attr.getattr())
2717 else:
2718 result.append(attr)
2719 return result
2722 def _iterateattr(attr):
2723 """perform next to a attr/changeattr"""
2724 if isinstance(attr, _changeattr):
2725 return attr.iterate()
2726 return attr
2729 def _iterateattrs(attrs):
2730 """perform next to a sequence of attr/changeattr"""
2731 if attrs is not None:
2732 result = []
2733 for attr in helper.ensuresequence(attrs):
2734 if isinstance(attr, _changeattr):
2735 result.append(attr.iterate())
2736 else:
2737 result.append(attr)
2738 return result
2741 class changecolor(changeattr):
2743 def __init__(self, palette):
2744 changeattr.__init__(self)
2745 self.palette = palette
2747 def attr(self, index):
2748 if self.counter != 1:
2749 return self.palette.getcolor(index/float(self.counter-1))
2750 else:
2751 return self.palette.getcolor(0)
2754 class _changecolorgray(changecolor):
2756 def __init__(self, palette=color.palette.Gray):
2757 changecolor.__init__(self, palette)
2759 _changecolorgrey = _changecolorgray
2762 class _changecolorreversegray(changecolor):
2764 def __init__(self, palette=color.palette.ReverseGray):
2765 changecolor.__init__(self, palette)
2767 _changecolorreversegrey = _changecolorreversegray
2770 class _changecolorredblack(changecolor):
2772 def __init__(self, palette=color.palette.RedBlack):
2773 changecolor.__init__(self, palette)
2776 class _changecolorblackred(changecolor):
2778 def __init__(self, palette=color.palette.BlackRed):
2779 changecolor.__init__(self, palette)
2782 class _changecolorredwhite(changecolor):
2784 def __init__(self, palette=color.palette.RedWhite):
2785 changecolor.__init__(self, palette)
2788 class _changecolorwhitered(changecolor):
2790 def __init__(self, palette=color.palette.WhiteRed):
2791 changecolor.__init__(self, palette)
2794 class _changecolorgreenblack(changecolor):
2796 def __init__(self, palette=color.palette.GreenBlack):
2797 changecolor.__init__(self, palette)
2800 class _changecolorblackgreen(changecolor):
2802 def __init__(self, palette=color.palette.BlackGreen):
2803 changecolor.__init__(self, palette)
2806 class _changecolorgreenwhite(changecolor):
2808 def __init__(self, palette=color.palette.GreenWhite):
2809 changecolor.__init__(self, palette)
2812 class _changecolorwhitegreen(changecolor):
2814 def __init__(self, palette=color.palette.WhiteGreen):
2815 changecolor.__init__(self, palette)
2818 class _changecolorblueblack(changecolor):
2820 def __init__(self, palette=color.palette.BlueBlack):
2821 changecolor.__init__(self, palette)
2824 class _changecolorblackblue(changecolor):
2826 def __init__(self, palette=color.palette.BlackBlue):
2827 changecolor.__init__(self, palette)
2830 class _changecolorbluewhite(changecolor):
2832 def __init__(self, palette=color.palette.BlueWhite):
2833 changecolor.__init__(self, palette)
2836 class _changecolorwhiteblue(changecolor):
2838 def __init__(self, palette=color.palette.WhiteBlue):
2839 changecolor.__init__(self, palette)
2842 class _changecolorredgreen(changecolor):
2844 def __init__(self, palette=color.palette.RedGreen):
2845 changecolor.__init__(self, palette)
2848 class _changecolorredblue(changecolor):
2850 def __init__(self, palette=color.palette.RedBlue):
2851 changecolor.__init__(self, palette)
2854 class _changecolorgreenred(changecolor):
2856 def __init__(self, palette=color.palette.GreenRed):
2857 changecolor.__init__(self, palette)
2860 class _changecolorgreenblue(changecolor):
2862 def __init__(self, palette=color.palette.GreenBlue):
2863 changecolor.__init__(self, palette)
2866 class _changecolorbluered(changecolor):
2868 def __init__(self, palette=color.palette.BlueRed):
2869 changecolor.__init__(self, palette)
2872 class _changecolorbluegreen(changecolor):
2874 def __init__(self, palette=color.palette.BlueGreen):
2875 changecolor.__init__(self, palette)
2878 class _changecolorrainbow(changecolor):
2880 def __init__(self, palette=color.palette.Rainbow):
2881 changecolor.__init__(self, palette)
2884 class _changecolorreverserainbow(changecolor):
2886 def __init__(self, palette=color.palette.ReverseRainbow):
2887 changecolor.__init__(self, palette)
2890 class _changecolorhue(changecolor):
2892 def __init__(self, palette=color.palette.Hue):
2893 changecolor.__init__(self, palette)
2896 class _changecolorreversehue(changecolor):
2898 def __init__(self, palette=color.palette.ReverseHue):
2899 changecolor.__init__(self, palette)
2902 changecolor.Gray = _changecolorgray
2903 changecolor.Grey = _changecolorgrey
2904 changecolor.Reversegray = _changecolorreversegray
2905 changecolor.Reversegrey = _changecolorreversegrey
2906 changecolor.RedBlack = _changecolorredblack
2907 changecolor.BlackRed = _changecolorblackred
2908 changecolor.RedWhite = _changecolorredwhite
2909 changecolor.WhiteRed = _changecolorwhitered
2910 changecolor.GreenBlack = _changecolorgreenblack
2911 changecolor.BlackGreen = _changecolorblackgreen
2912 changecolor.GreenWhite = _changecolorgreenwhite
2913 changecolor.WhiteGreen = _changecolorwhitegreen
2914 changecolor.BlueBlack = _changecolorblueblack
2915 changecolor.BlackBlue = _changecolorblackblue
2916 changecolor.BlueWhite = _changecolorbluewhite
2917 changecolor.WhiteBlue = _changecolorwhiteblue
2918 changecolor.RedGreen = _changecolorredgreen
2919 changecolor.RedBlue = _changecolorredblue
2920 changecolor.GreenRed = _changecolorgreenred
2921 changecolor.GreenBlue = _changecolorgreenblue
2922 changecolor.BlueRed = _changecolorbluered
2923 changecolor.BlueGreen = _changecolorbluegreen
2924 changecolor.Rainbow = _changecolorrainbow
2925 changecolor.ReverseRainbow = _changecolorreverserainbow
2926 changecolor.Hue = _changecolorhue
2927 changecolor.ReverseHue = _changecolorreversehue
2930 class changesequence(changeattr):
2931 """cycles through a sequence"""
2933 def __init__(self, *sequence):
2934 changeattr.__init__(self)
2935 if not len(sequence):
2936 sequence = self.defaultsequence
2937 self.sequence = sequence
2939 def attr(self, index):
2940 return self.sequence[index % len(self.sequence)]
2943 class changelinestyle(changesequence):
2944 defaultsequence = (canvas.linestyle.solid,
2945 canvas.linestyle.dashed,
2946 canvas.linestyle.dotted,
2947 canvas.linestyle.dashdotted)
2950 class changestrokedfilled(changesequence):
2951 defaultsequence = (canvas.stroked(), canvas.filled())
2954 class changefilledstroked(changesequence):
2955 defaultsequence = (canvas.filled(), canvas.stroked())
2959 ################################################################################
2960 # styles
2961 ################################################################################
2964 class symbol:
2966 def cross(self, x, y):
2967 return (path._moveto(x-0.5*self._size, y-0.5*self._size),
2968 path._lineto(x+0.5*self._size, y+0.5*self._size),
2969 path._moveto(x-0.5*self._size, y+0.5*self._size),
2970 path._lineto(x+0.5*self._size, y-0.5*self._size))
2972 def plus(self, x, y):
2973 return (path._moveto(x-0.707106781*self._size, y),
2974 path._lineto(x+0.707106781*self._size, y),
2975 path._moveto(x, y-0.707106781*self._size),
2976 path._lineto(x, y+0.707106781*self._size))
2978 def square(self, x, y):
2979 return (path._moveto(x-0.5*self._size, y-0.5 * self._size),
2980 path._lineto(x+0.5*self._size, y-0.5 * self._size),
2981 path._lineto(x+0.5*self._size, y+0.5 * self._size),
2982 path._lineto(x-0.5*self._size, y+0.5 * self._size),
2983 path.closepath())
2985 def triangle(self, x, y):
2986 return (path._moveto(x-0.759835685*self._size, y-0.438691337*self._size),
2987 path._lineto(x+0.759835685*self._size, y-0.438691337*self._size),
2988 path._lineto(x, y+0.877382675*self._size),
2989 path.closepath())
2991 def circle(self, x, y):
2992 return (path._arc(x, y, 0.564189583*self._size, 0, 360),
2993 path.closepath())
2995 def diamond(self, x, y):
2996 return (path._moveto(x-0.537284965*self._size, y),
2997 path._lineto(x, y-0.930604859*self._size),
2998 path._lineto(x+0.537284965*self._size, y),
2999 path._lineto(x, y+0.930604859*self._size),
3000 path.closepath())
3002 def __init__(self, symbol=helper.nodefault,
3003 size="0.2 cm", symbolattrs=canvas.stroked(),
3004 errorscale=0.5, errorbarattrs=(),
3005 lineattrs=None):
3006 self.size_str = size
3007 if symbol is helper.nodefault:
3008 self._symbol = changesymbol.cross()
3009 else:
3010 self._symbol = symbol
3011 self._symbolattrs = symbolattrs
3012 self.errorscale = errorscale
3013 self._errorbarattrs = errorbarattrs
3014 self._lineattrs = lineattrs
3016 def iteratedict(self):
3017 result = {}
3018 result["symbol"] = _iterateattr(self._symbol)
3019 result["size"] = _iterateattr(self.size_str)
3020 result["symbolattrs"] = _iterateattrs(self._symbolattrs)
3021 result["errorscale"] = _iterateattr(self.errorscale)
3022 result["errorbarattrs"] = _iterateattrs(self._errorbarattrs)
3023 result["lineattrs"] = _iterateattrs(self._lineattrs)
3024 return result
3026 def iterate(self):
3027 return symbol(**self.iteratedict())
3029 def othercolumnkey(self, key, index):
3030 raise ValueError("unsuitable key '%s'" % key)
3032 def setcolumns(self, graph, columns):
3033 def checkpattern(key, index, pattern, iskey, isindex):
3034 if key is not None:
3035 match = pattern.match(key)
3036 if match:
3037 if isindex is not None: raise ValueError("multiple key specification")
3038 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
3039 key = None
3040 iskey = match.groups()[0]
3041 isindex = index
3042 return key, iskey, isindex
3044 self.xi = self.xmini = self.xmaxi = None
3045 self.dxi = self.dxmini = self.dxmaxi = None
3046 self.yi = self.ymini = self.ymaxi = None
3047 self.dyi = self.dymini = self.dymaxi = None
3048 self.xkey = self.ykey = None
3049 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
3050 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3051 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3052 XMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
3053 YMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
3054 XMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
3055 YMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
3056 DXPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3057 DYPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3058 DXMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
3059 DYMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
3060 DXMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
3061 DYMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
3062 for key, index in columns.items():
3063 key, self.xkey, self.xi = checkpattern(key, index, XPattern, self.xkey, self.xi)
3064 key, self.ykey, self.yi = checkpattern(key, index, YPattern, self.ykey, self.yi)
3065 key, self.xkey, self.xmini = checkpattern(key, index, XMinPattern, self.xkey, self.xmini)
3066 key, self.ykey, self.ymini = checkpattern(key, index, YMinPattern, self.ykey, self.ymini)
3067 key, self.xkey, self.xmaxi = checkpattern(key, index, XMaxPattern, self.xkey, self.xmaxi)
3068 key, self.ykey, self.ymaxi = checkpattern(key, index, YMaxPattern, self.ykey, self.ymaxi)
3069 key, self.xkey, self.dxi = checkpattern(key, index, DXPattern, self.xkey, self.dxi)
3070 key, self.ykey, self.dyi = checkpattern(key, index, DYPattern, self.ykey, self.dyi)
3071 key, self.xkey, self.dxmini = checkpattern(key, index, DXMinPattern, self.xkey, self.dxmini)
3072 key, self.ykey, self.dymini = checkpattern(key, index, DYMinPattern, self.ykey, self.dymini)
3073 key, self.xkey, self.dxmaxi = checkpattern(key, index, DXMaxPattern, self.xkey, self.dxmaxi)
3074 key, self.ykey, self.dymaxi = checkpattern(key, index, DYMaxPattern, self.ykey, self.dymaxi)
3075 if key is not None:
3076 self.othercolumnkey(key, index)
3077 if None in (self.xkey, self.ykey): raise ValueError("incomplete axis specification")
3078 if (len(filter(None, (self.xmini, self.dxmini, self.dxi))) > 1 or
3079 len(filter(None, (self.ymini, self.dymini, self.dyi))) > 1 or
3080 len(filter(None, (self.xmaxi, self.dxmaxi, self.dxi))) > 1 or
3081 len(filter(None, (self.ymaxi, self.dymaxi, self.dyi))) > 1):
3082 raise ValueError("multiple errorbar definition")
3083 if ((self.xi is None and self.dxi is not None) or
3084 (self.yi is None and self.dyi is not None) or
3085 (self.xi is None and self.dxmini is not None) or
3086 (self.yi is None and self.dymini is not None) or
3087 (self.xi is None and self.dxmaxi is not None) or
3088 (self.yi is None and self.dymaxi is not None)):
3089 raise ValueError("errorbar definition start value missing")
3090 self.xaxis = graph.axes[self.xkey]
3091 self.yaxis = graph.axes[self.ykey]
3093 def minmidmax(self, point, i, mini, maxi, di, dmini, dmaxi):
3094 min = max = mid = None
3095 try:
3096 mid = point[i] + 0.0
3097 except (TypeError, ValueError):
3098 pass
3099 try:
3100 if di is not None: min = point[i] - point[di]
3101 elif dmini is not None: min = point[i] - point[dmini]
3102 elif mini is not None: min = point[mini] + 0.0
3103 except (TypeError, ValueError):
3104 pass
3105 try:
3106 if di is not None: max = point[i] + point[di]
3107 elif dmaxi is not None: max = point[i] + point[dmaxi]
3108 elif maxi is not None: max = point[maxi] + 0.0
3109 except (TypeError, ValueError):
3110 pass
3111 if mid is not None:
3112 if min is not None and min > mid: raise ValueError("minimum error in errorbar")
3113 if max is not None and max < mid: raise ValueError("maximum error in errorbar")
3114 else:
3115 if min is not None and max is not None and min > max: raise ValueError("minimum/maximum error in errorbar")
3116 return min, mid, max
3118 def keyrange(self, points, i, mini, maxi, di, dmini, dmaxi):
3119 allmin = allmax = None
3120 if filter(None, (mini, maxi, di, dmini, dmaxi)) is not None:
3121 for point in points:
3122 min, mid, max = self.minmidmax(point, i, mini, maxi, di, dmini, dmaxi)
3123 if min is not None and (allmin is None or min < allmin): allmin = min
3124 if mid is not None and (allmin is None or mid < allmin): allmin = mid
3125 if mid is not None and (allmax is None or mid > allmax): allmax = mid
3126 if max is not None and (allmax is None or max > allmax): allmax = max
3127 else:
3128 for point in points:
3129 try:
3130 value = point[i] + 0.0
3131 if allmin is None or point[i] < allmin: allmin = point[i]
3132 if allmax is None or point[i] > allmax: allmax = point[i]
3133 except (TypeError, ValueError):
3134 pass
3135 return allmin, allmax
3137 def getranges(self, points):
3138 xmin, xmax = self.keyrange(points, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
3139 ymin, ymax = self.keyrange(points, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
3140 return {self.xkey: (xmin, xmax), self.ykey: (ymin, ymax)}
3142 def _drawerrorbar(self, graph, topleft, top, topright,
3143 left, center, right,
3144 bottomleft, bottom, bottomright, point=None):
3145 if left is not None:
3146 if right is not None:
3147 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3148 left2 = graph._addpos(*(left+(0, self._errorsize)))
3149 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3150 right2 = graph._addpos(*(right+(0, self._errorsize)))
3151 graph.stroke(path.path(path._moveto(*left1),
3152 graph._connect(*(left1+left2)),
3153 path._moveto(*left),
3154 graph._connect(*(left+right)),
3155 path._moveto(*right1),
3156 graph._connect(*(right1+right2))),
3157 *self.errorbarattrs)
3158 elif center is not None:
3159 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3160 left2 = graph._addpos(*(left+(0, self._errorsize)))
3161 graph.stroke(path.path(path._moveto(*left1),
3162 graph._connect(*(left1+left2)),
3163 path._moveto(*left),
3164 graph._connect(*(left+center))),
3165 *self.errorbarattrs)
3166 else:
3167 left1 = graph._addpos(*(left+(0, -self._errorsize)))
3168 left2 = graph._addpos(*(left+(0, self._errorsize)))
3169 left3 = graph._addpos(*(left+(self._errorsize, 0)))
3170 graph.stroke(path.path(path._moveto(*left1),
3171 graph._connect(*(left1+left2)),
3172 path._moveto(*left),
3173 graph._connect(*(left+left3))),
3174 *self.errorbarattrs)
3175 if right is not None and left is None:
3176 if center is not None:
3177 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3178 right2 = graph._addpos(*(right+(0, self._errorsize)))
3179 graph.stroke(path.path(path._moveto(*right1),
3180 graph._connect(*(right1+right2)),
3181 path._moveto(*right),
3182 graph._connect(*(right+center))),
3183 *self.errorbarattrs)
3184 else:
3185 right1 = graph._addpos(*(right+(0, -self._errorsize)))
3186 right2 = graph._addpos(*(right+(0, self._errorsize)))
3187 right3 = graph._addpos(*(right+(-self._errorsize, 0)))
3188 graph.stroke(path.path(path._moveto(*right1),
3189 graph._connect(*(right1+right2)),
3190 path._moveto(*right),
3191 graph._connect(*(right+right3))),
3192 *self.errorbarattrs)
3194 if bottom is not None:
3195 if top is not None:
3196 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3197 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3198 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3199 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3200 graph.stroke(path.path(path._moveto(*bottom1),
3201 graph._connect(*(bottom1+bottom2)),
3202 path._moveto(*bottom),
3203 graph._connect(*(bottom+top)),
3204 path._moveto(*top1),
3205 graph._connect(*(top1+top2))),
3206 *self.errorbarattrs)
3207 elif center is not None:
3208 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3209 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3210 graph.stroke(path.path(path._moveto(*bottom1),
3211 graph._connect(*(bottom1+bottom2)),
3212 path._moveto(*bottom),
3213 graph._connect(*(bottom+center))),
3214 *self.errorbarattrs)
3215 else:
3216 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
3217 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
3218 bottom3 = graph._addpos(*(bottom+(0, self._errorsize)))
3219 graph.stroke(path.path(path._moveto(*bottom1),
3220 graph._connect(*(bottom1+bottom2)),
3221 path._moveto(*bottom),
3222 graph._connect(*(bottom+bottom3))),
3223 *self.errorbarattrs)
3224 if top is not None and bottom is None:
3225 if center is not None:
3226 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3227 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3228 graph.stroke(path.path(path._moveto(*top1),
3229 graph._connect(*(top1+top2)),
3230 path._moveto(*top),
3231 graph._connect(*(top+center))),
3232 *self.errorbarattrs)
3233 else:
3234 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
3235 top2 = graph._addpos(*(top+(self._errorsize, 0)))
3236 top3 = graph._addpos(*(top+(0, -self._errorsize)))
3237 graph.stroke(path.path(path._moveto(*top1),
3238 graph._connect(*(top1+top2)),
3239 path._moveto(*top),
3240 graph._connect(*(top+top3))),
3241 *self.errorbarattrs)
3242 if bottomleft is not None:
3243 if topleft is not None and bottomright is None:
3244 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
3245 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
3246 graph.stroke(path.path(path._moveto(*bottomleft1),
3247 graph._connect(*(bottomleft1+bottomleft)),
3248 graph._connect(*(bottomleft+topleft)),
3249 graph._connect(*(topleft+topleft1))),
3250 *self.errorbarattrs)
3251 elif bottomright is not None and topleft is None:
3252 bottomleft1 = graph._addpos(*(bottomleft+(0, self._errorsize)))
3253 bottomright1 = graph._addpos(*(bottomright+(0, self._errorsize)))
3254 graph.stroke(path.path(path._moveto(*bottomleft1),
3255 graph._connect(*(bottomleft1+bottomleft)),
3256 graph._connect(*(bottomleft+bottomright)),
3257 graph._connect(*(bottomright+bottomright1))),
3258 *self.errorbarattrs)
3259 elif bottomright is None and topleft is None:
3260 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
3261 bottomleft2 = graph._addpos(*(bottomleft+(0, self._errorsize)))
3262 graph.stroke(path.path(path._moveto(*bottomleft1),
3263 graph._connect(*(bottomleft1+bottomleft)),
3264 graph._connect(*(bottomleft+bottomleft2))),
3265 *self.errorbarattrs)
3266 if topright is not None:
3267 if bottomright is not None and topleft is None:
3268 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
3269 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
3270 graph.stroke(path.path(path._moveto(*topright1),
3271 graph._connect(*(topright1+topright)),
3272 graph._connect(*(topright+bottomright)),
3273 graph._connect(*(bottomright+bottomright1))),
3274 *self.errorbarattrs)
3275 elif topleft is not None and bottomright is None:
3276 topright1 = graph._addpos(*(topright+(0, -self._errorsize)))
3277 topleft1 = graph._addpos(*(topleft+(0, -self._errorsize)))
3278 graph.stroke(path.path(path._moveto(*topright1),
3279 graph._connect(*(topright1+topright)),
3280 graph._connect(*(topright+topleft)),
3281 graph._connect(*(topleft+topleft1))),
3282 *self.errorbarattrs)
3283 elif topleft is None and bottomright is None:
3284 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
3285 topright2 = graph._addpos(*(topright+(0, -self._errorsize)))
3286 graph.stroke(path.path(path._moveto(*topright1),
3287 graph._connect(*(topright1+topright)),
3288 graph._connect(*(topright+topright2))),
3289 *self.errorbarattrs)
3290 if bottomright is not None and bottomleft is None and topright is None:
3291 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
3292 bottomright2 = graph._addpos(*(bottomright+(0, self._errorsize)))
3293 graph.stroke(path.path(path._moveto(*bottomright1),
3294 graph._connect(*(bottomright1+bottomright)),
3295 graph._connect(*(bottomright+bottomright2))),
3296 *self.errorbarattrs)
3297 if topleft is not None and bottomleft is None and topright is None:
3298 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
3299 topleft2 = graph._addpos(*(topleft+(0, -self._errorsize)))
3300 graph.stroke(path.path(path._moveto(*topleft1),
3301 graph._connect(*(topleft1+topleft)),
3302 graph._connect(*(topleft+topleft2))),
3303 *self.errorbarattrs)
3304 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
3305 graph.stroke(path.path(path._moveto(*bottomleft),
3306 graph._connect(*(bottomleft+bottomright)),
3307 graph._connect(*(bottomright+topright)),
3308 graph._connect(*(topright+topleft)),
3309 path.closepath()),
3310 *self.errorbarattrs)
3312 def _drawsymbol(self, canvas, x, y, point=None):
3313 canvas.draw(path.path(*self.symbol(self, x, y)), *self.symbolattrs)
3315 def drawsymbol(self, canvas, x, y, point=None):
3316 self._drawsymbol(canvas, unit.topt(x), unit.topt(y), point)
3318 def key(self, c, x, y, width, height):
3319 if self._symbolattrs is not None:
3320 self._drawsymbol(c, x + 0.5 * width, y + 0.5 * height)
3321 if self._lineattrs is not None:
3322 c.stroke(path._line(x, y + 0.5 * height, x + width, y + 0.5 * height), *self.lineattrs)
3324 def drawpoints(self, graph, points):
3325 xaxismin, xaxismax = self.xaxis.getdatarange()
3326 yaxismin, yaxismax = self.yaxis.getdatarange()
3327 self.size = unit.length(_getattr(self.size_str), default_type="v")
3328 self._size = unit.topt(self.size)
3329 self.symbol = _getattr(self._symbol)
3330 self.symbolattrs = _getattrs(helper.ensuresequence(self._symbolattrs))
3331 self.errorbarattrs = _getattrs(helper.ensuresequence(self._errorbarattrs))
3332 self._errorsize = self.errorscale * self._size
3333 self.errorsize = self.errorscale * self.size
3334 self.lineattrs = _getattrs(helper.ensuresequence(self._lineattrs))
3335 if self._lineattrs is not None:
3336 clipcanvas = graph.clipcanvas()
3337 lineels = []
3338 haserror = filter(None, (self.xmini, self.ymini, self.xmaxi, self.ymaxi,
3339 self.dxi, self.dyi, self.dxmini, self.dymini, self.dxmaxi, self.dymaxi)) is not None
3340 moveto = 1
3341 for point in points:
3342 drawsymbol = 1
3343 xmin, x, xmax = self.minmidmax(point, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
3344 ymin, y, ymax = self.minmidmax(point, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
3345 if x is not None and x < xaxismin: drawsymbol = 0
3346 elif x is not None and x > xaxismax: drawsymbol = 0
3347 elif y is not None and y < yaxismin: drawsymbol = 0
3348 elif y is not None and y > yaxismax: drawsymbol = 0
3349 elif haserror:
3350 if xmin is not None and xmin < xaxismin: drawsymbol = 0
3351 elif xmax is not None and xmax < xaxismin: drawsymbol = 0
3352 elif xmax is not None and xmax > xaxismax: drawsymbol = 0
3353 elif xmin is not None and xmin > xaxismax: drawsymbol = 0
3354 elif ymin is not None and ymin < yaxismin: drawsymbol = 0
3355 elif ymax is not None and ymax < yaxismin: drawsymbol = 0
3356 elif ymax is not None and ymax > yaxismax: drawsymbol = 0
3357 elif ymin is not None and ymin > yaxismax: drawsymbol = 0
3358 xpos=ypos=topleft=top=topright=left=center=right=bottomleft=bottom=bottomright=None
3359 if x is not None and y is not None:
3360 try:
3361 center = xpos, ypos = graph._pos(x, y, xaxis=self.xaxis, yaxis=self.yaxis)
3362 except (ValueError, OverflowError): # XXX: exceptions???
3363 pass
3364 if haserror:
3365 if y is not None:
3366 if xmin is not None: left = graph._pos(xmin, y, xaxis=self.xaxis, yaxis=self.yaxis)
3367 if xmax is not None: right = graph._pos(xmax, y, xaxis=self.xaxis, yaxis=self.yaxis)
3368 if x is not None:
3369 if ymax is not None: top = graph._pos(x, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3370 if ymin is not None: bottom = graph._pos(x, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3371 if x is None or y is None:
3372 if ymax is not None:
3373 if xmin is not None: topleft = graph._pos(xmin, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3374 if xmax is not None: topright = graph._pos(xmax, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
3375 if ymin is not None:
3376 if xmin is not None: bottomleft = graph._pos(xmin, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3377 if xmax is not None: bottomright = graph._pos(xmax, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
3378 if drawsymbol:
3379 if self._errorbarattrs is not None and haserror:
3380 self._drawerrorbar(graph, topleft, top, topright,
3381 left, center, right,
3382 bottomleft, bottom, bottomright, point)
3383 if self._symbolattrs is not None and xpos is not None and ypos is not None:
3384 self._drawsymbol(graph, xpos, ypos, point)
3385 if xpos is not None and ypos is not None:
3386 if moveto:
3387 lineels.append(path._moveto(xpos, ypos))
3388 moveto = 0
3389 else:
3390 lineels.append(path._lineto(xpos, ypos))
3391 else:
3392 moveto = 1
3393 self.path = path.path(*lineels)
3394 if self._lineattrs is not None:
3395 clipcanvas.stroke(self.path, *self.lineattrs)
3398 class changesymbol(changesequence): pass
3401 class _changesymbolcross(changesymbol):
3402 defaultsequence = (symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond)
3405 class _changesymbolplus(changesymbol):
3406 defaultsequence = (symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross)
3409 class _changesymbolsquare(changesymbol):
3410 defaultsequence = (symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus)
3413 class _changesymboltriangle(changesymbol):
3414 defaultsequence = (symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square)
3417 class _changesymbolcircle(changesymbol):
3418 defaultsequence = (symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle)
3421 class _changesymboldiamond(changesymbol):
3422 defaultsequence = (symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle)
3425 class _changesymbolsquaretwice(changesymbol):
3426 defaultsequence = (symbol.square, symbol.square, symbol.triangle, symbol.triangle,
3427 symbol.circle, symbol.circle, symbol.diamond, symbol.diamond)
3430 class _changesymboltriangletwice(changesymbol):
3431 defaultsequence = (symbol.triangle, symbol.triangle, symbol.circle, symbol.circle,
3432 symbol.diamond, symbol.diamond, symbol.square, symbol.square)
3435 class _changesymbolcircletwice(changesymbol):
3436 defaultsequence = (symbol.circle, symbol.circle, symbol.diamond, symbol.diamond,
3437 symbol.square, symbol.square, symbol.triangle, symbol.triangle)
3440 class _changesymboldiamondtwice(changesymbol):
3441 defaultsequence = (symbol.diamond, symbol.diamond, symbol.square, symbol.square,
3442 symbol.triangle, symbol.triangle, symbol.circle, symbol.circle)
3445 changesymbol.cross = _changesymbolcross
3446 changesymbol.plus = _changesymbolplus
3447 changesymbol.square = _changesymbolsquare
3448 changesymbol.triangle = _changesymboltriangle
3449 changesymbol.circle = _changesymbolcircle
3450 changesymbol.diamond = _changesymboldiamond
3451 changesymbol.squaretwice = _changesymbolsquaretwice
3452 changesymbol.triangletwice = _changesymboltriangletwice
3453 changesymbol.circletwice = _changesymbolcircletwice
3454 changesymbol.diamondtwice = _changesymboldiamondtwice
3457 class line(symbol):
3459 def __init__(self, lineattrs=helper.nodefault):
3460 if lineattrs is helper.nodefault:
3461 lineattrs = (changelinestyle(), canvas.linejoin.round)
3462 symbol.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
3465 class rect(symbol):
3467 def __init__(self, palette=color.palette.Gray):
3468 self.palette = palette
3469 self.colorindex = None
3470 symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
3472 def iterate(self):
3473 raise RuntimeError("style is not iterateable")
3475 def othercolumnkey(self, key, index):
3476 if key == "color":
3477 self.colorindex = index
3478 else:
3479 symbol.othercolumnkey(self, key, index)
3481 def _drawerrorbar(self, graph, topleft, top, topright,
3482 left, center, right,
3483 bottomleft, bottom, bottomright, point=None):
3484 color = point[self.colorindex]
3485 if color is not None:
3486 if color != self.lastcolor:
3487 self.rectclipcanvas.set(self.palette.getcolor(color))
3488 if bottom is not None and left is not None:
3489 bottomleft = left[0], bottom[1]
3490 if bottom is not None and right is not None:
3491 bottomright = right[0], bottom[1]
3492 if top is not None and right is not None:
3493 topright = right[0], top[1]
3494 if top is not None and left is not None:
3495 topleft = left[0], top[1]
3496 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
3497 self.rectclipcanvas.fill(path.path(path._moveto(*bottomleft),
3498 graph._connect(*(bottomleft+bottomright)),
3499 graph._connect(*(bottomright+topright)),
3500 graph._connect(*(topright+topleft)),
3501 path.closepath()))
3503 def drawpoints(self, graph, points):
3504 if self.colorindex is None:
3505 raise RuntimeError("column 'color' not set")
3506 self.lastcolor = None
3507 self.rectclipcanvas = graph.clipcanvas()
3508 symbol.drawpoints(self, graph, points)
3510 def key(self, c, x, y, width, height):
3511 raise RuntimeError("style doesn't yet provide a key")
3514 class text(symbol):
3516 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=textmodule.halign.center, **args):
3517 self.textindex = None
3518 self.textdx_str = textdx
3519 self.textdy_str = textdy
3520 self._textattrs = textattrs
3521 symbol.__init__(self, **args)
3523 def iteratedict(self):
3524 result = symbol.iteratedict()
3525 result["textattrs"] = _iterateattr(self._textattrs)
3526 return result
3528 def iterate(self):
3529 return textsymbol(**self.iteratedict())
3531 def othercolumnkey(self, key, index):
3532 if key == "text":
3533 self.textindex = index
3534 else:
3535 symbol.othercolumnkey(self, key, index)
3537 def _drawsymbol(self, graph, x, y, point=None):
3538 symbol._drawsymbol(self, graph, x, y, point)
3539 if None not in (x, y, point[self.textindex], self._textattrs):
3540 graph._text(x + self._textdx, y + self._textdy, str(point[self.textindex]), *helper.ensuresequence(self.textattrs))
3542 def drawpoints(self, graph, points):
3543 self.textdx = unit.length(_getattr(self.textdx_str), default_type="v")
3544 self.textdy = unit.length(_getattr(self.textdy_str), default_type="v")
3545 self._textdx = unit.topt(self.textdx)
3546 self._textdy = unit.topt(self.textdy)
3547 if self._textattrs is not None:
3548 self.textattrs = _getattr(self._textattrs)
3549 if self.textindex is None:
3550 raise RuntimeError("column 'text' not set")
3551 symbol.drawpoints(self, graph, points)
3553 def key(self, c, x, y, width, height):
3554 raise RuntimeError("style doesn't yet provide a key")
3557 class arrow(symbol):
3559 def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
3560 self.linelength_str = linelength
3561 self.arrowsize_str = arrowsize
3562 self.arrowattrs = arrowattrs
3563 self.arrowdict = arrowdict
3564 self.epsilon = epsilon
3565 self.sizeindex = self.angleindex = None
3566 symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
3568 def iterate(self):
3569 raise RuntimeError("style is not iterateable")
3571 def othercolumnkey(self, key, index):
3572 if key == "size":
3573 self.sizeindex = index
3574 elif key == "angle":
3575 self.angleindex = index
3576 else:
3577 symbol.othercolumnkey(self, key, index)
3579 def _drawsymbol(self, graph, x, y, point=None):
3580 if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
3581 if point[self.sizeindex] > self.epsilon:
3582 dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
3583 x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
3584 y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
3585 x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
3586 y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
3587 graph.stroke(path.line(x1, y1, x2, y2),
3588 canvas.earrow(self.arrowsize*point[self.sizeindex],
3589 **self.arrowdict),
3590 *helper.ensuresequence(self.arrowattrs))
3592 def drawpoints(self, graph, points):
3593 self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
3594 self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
3595 self._arrowsize = unit.topt(self.arrowsize)
3596 self._linelength = unit.topt(self.linelength)
3597 if self.sizeindex is None:
3598 raise RuntimeError("column 'size' not set")
3599 if self.angleindex is None:
3600 raise RuntimeError("column 'angle' not set")
3601 symbol.drawpoints(self, graph, points)
3603 def key(self, c, x, y, width, height):
3604 raise RuntimeError("style doesn't yet provide a key")
3607 class _bariterator(changeattr):
3609 def attr(self, index):
3610 return index, self.counter
3613 class bar:
3615 def __init__(self, fromzero=1, stacked=0, skipmissing=1, xbar=0,
3616 barattrs=(canvas.stroked(color.gray.black), changecolor.Rainbow()),
3617 _bariterator=_bariterator(), _previousbar=None):
3618 self.fromzero = fromzero
3619 self.stacked = stacked
3620 self.skipmissing = skipmissing
3621 self.xbar = xbar
3622 self._barattrs = barattrs
3623 self.bariterator = _bariterator
3624 self.previousbar = _previousbar
3626 def iteratedict(self):
3627 result = {}
3628 result["barattrs"] = _iterateattrs(self._barattrs)
3629 return result
3631 def iterate(self):
3632 return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
3633 _bariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
3635 def setcolumns(self, graph, columns):
3636 def checkpattern(key, index, pattern, iskey, isindex):
3637 if key is not None:
3638 match = pattern.match(key)
3639 if match:
3640 if isindex is not None: raise ValueError("multiple key specification")
3641 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
3642 key = None
3643 iskey = match.groups()[0]
3644 isindex = index
3645 return key, iskey, isindex
3647 xkey = ykey = None
3648 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
3649 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3650 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3651 xi = yi = None
3652 for key, index in columns.items():
3653 key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
3654 key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
3655 if key is not None:
3656 self.othercolumnkey(key, index)
3657 if None in (xkey, ykey): raise ValueError("incomplete axis specification")
3658 if self.xbar:
3659 self.nkey, self.ni = ykey, yi
3660 self.vkey, self.vi = xkey, xi
3661 else:
3662 self.nkey, self.ni = xkey, xi
3663 self.vkey, self.vi = ykey, yi
3664 self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
3666 def getranges(self, points):
3667 index, count = _getattr(self.bariterator)
3668 if count != 1 and self.stacked != 1:
3669 if self.stacked > 1:
3670 index = divmod(index, self.stacked)[0] # TODO: use this
3672 vmin = vmax = None
3673 for point in points:
3674 if not self.skipmissing:
3675 if count == 1:
3676 self.naxis.setname(point[self.ni])
3677 else:
3678 self.naxis.setname(point[self.ni], index)
3679 try:
3680 v = point[self.vi] + 0.0
3681 if vmin is None or v < vmin: vmin = v
3682 if vmax is None or v > vmax: vmax = v
3683 except (TypeError, ValueError):
3684 pass
3685 else:
3686 if self.skipmissing:
3687 if count == 1:
3688 self.naxis.setname(point[self.ni])
3689 else:
3690 self.naxis.setname(point[self.ni], index)
3691 if self.fromzero:
3692 if vmin > 0: vmin = 0
3693 if vmax < 0: vmax = 0
3694 return {self.vkey: (vmin, vmax)}
3696 def drawpoints(self, graph, points):
3697 index, count = _getattr(self.bariterator)
3698 dostacked = (self.stacked != 0 and
3699 (self.stacked == 1 or divmod(index, self.stacked)[1]) and
3700 (self.stacked != 1 or index))
3701 if self.stacked > 1:
3702 index = divmod(index, self.stacked)[0]
3703 vmin, vmax = self.vaxis.getdatarange()
3704 self.barattrs = _getattrs(helper.ensuresequence(self._barattrs))
3705 if self.stacked:
3706 self.stackedvalue = {}
3707 for point in points:
3708 try:
3709 n = point[self.ni]
3710 v = point[self.vi]
3711 if self.stacked:
3712 self.stackedvalue[n] = v
3713 if count != 1 and self.stacked != 1:
3714 minid = (n, index, 0)
3715 maxid = (n, index, 1)
3716 else:
3717 minid = (n, 0)
3718 maxid = (n, 1)
3719 if self.xbar:
3720 x1pos, y1pos = graph._pos(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
3721 x2pos, y2pos = graph._pos(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
3722 else:
3723 x1pos, y1pos = graph._pos(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
3724 x2pos, y2pos = graph._pos(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
3725 if dostacked:
3726 if self.xbar:
3727 x3pos, y3pos = graph._pos(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
3728 x4pos, y4pos = graph._pos(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
3729 else:
3730 x3pos, y3pos = graph._pos(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
3731 x4pos, y4pos = graph._pos(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
3732 else:
3733 if self.fromzero:
3734 if self.xbar:
3735 x3pos, y3pos = graph._pos(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
3736 x4pos, y4pos = graph._pos(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
3737 else:
3738 x3pos, y3pos = graph._pos(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
3739 x4pos, y4pos = graph._pos(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
3740 else:
3741 x3pos, y3pos = self.naxis._vtickpoint(self.naxis, self.naxis.convert(maxid))
3742 x4pos, y4pos = self.naxis._vtickpoint(self.naxis, self.naxis.convert(minid))
3743 graph.fill(path.path(path._moveto(x1pos, y1pos),
3744 graph._connect(x1pos, y1pos, x2pos, y2pos),
3745 graph._connect(x2pos, y2pos, x3pos, y3pos),
3746 graph._connect(x3pos, y3pos, x4pos, y4pos),
3747 graph._connect(x4pos, y4pos, x1pos, y1pos), # no closepath (might not be straight)
3748 path.closepath()), *self.barattrs)
3749 except (TypeError, ValueError): pass
3751 def key(self, c, x, y, width, height):
3752 c.fill(path._rect(x, y, width, height), *self.barattrs)
3755 #class surface:
3757 # def setcolumns(self, graph, columns):
3758 # self.columns = columns
3760 # def getranges(self, points):
3761 # return {"x": (0, 10), "y": (0, 10), "z": (0, 1)}
3763 # def drawpoints(self, graph, points):
3764 # pass
3768 ################################################################################
3769 # data
3770 ################################################################################
3773 class data:
3775 defaultstyle = symbol
3777 def __init__(self, file, title=helper.nodefault, context={}, **columns):
3778 self.title = title
3779 if helper.isstring(file):
3780 self.data = datamodule.datafile(file)
3781 else:
3782 self.data = file
3783 if title is helper.nodefault:
3784 self.title = "(unknown)"
3785 else:
3786 self.title = title
3787 self.columns = {}
3788 for key, column in columns.items():
3789 try:
3790 self.columns[key] = self.data.getcolumnno(column)
3791 except datamodule.ColumnError:
3792 self.columns[key] = len(self.data.titles)
3793 self.data.addcolumn(column, context=context)
3795 def setstyle(self, graph, style):
3796 self.style = style
3797 self.style.setcolumns(graph, self.columns)
3799 def getranges(self):
3800 return self.style.getranges(self.data.data)
3802 def setranges(self, ranges):
3803 pass
3805 def draw(self, graph):
3806 self.style.drawpoints(graph, self.data.data)
3809 class function:
3811 defaultstyle = line
3813 def __init__(self, expression, title=helper.nodefault, min=None, max=None, points=100, parser=mathtree.parser(), context={}):
3814 if title is helper.nodefault:
3815 self.title = expression
3816 else:
3817 self.title = title
3818 self.min = min
3819 self.max = max
3820 self.points = points
3821 self.context = context
3822 self.result, expression = expression.split("=")
3823 self.mathtree = parser.parse(expression)
3824 self.variable = None
3825 self.evalranges = 0
3827 def setstyle(self, graph, style):
3828 for variable in self.mathtree.VarList():
3829 if variable in graph.axes.keys():
3830 if self.variable is None:
3831 self.variable = variable
3832 else:
3833 raise ValueError("multiple variables found")
3834 if self.variable is None:
3835 raise ValueError("no variable found")
3836 self.xaxis = graph.axes[self.variable]
3837 self.style = style
3838 self.style.setcolumns(graph, {self.variable: 0, self.result: 1})
3840 def getranges(self):
3841 if self.evalranges:
3842 return self.style.getranges(self.data)
3843 if None not in (self.min, self.max):
3844 return {self.variable: (self.min, self.max)}
3846 def setranges(self, ranges):
3847 if ranges.has_key(self.variable):
3848 min, max = ranges[self.variable]
3849 if self.min is not None: min = self.min
3850 if self.max is not None: max = self.max
3851 vmin = self.xaxis.convert(min)
3852 vmax = self.xaxis.convert(max)
3853 self.data = []
3854 for i in range(self.points):
3855 self.context[self.variable] = x = self.xaxis.invert(vmin + (vmax-vmin)*i / (self.points-1.0))
3856 try:
3857 y = self.mathtree.Calc(**self.context)
3858 except (ArithmeticError, ValueError):
3859 y = None
3860 self.data.append((x, y))
3861 self.evalranges = 1
3863 def draw(self, graph):
3864 self.style.drawpoints(graph, self.data)
3867 class paramfunction:
3869 defaultstyle = line
3871 def __init__(self, varname, min, max, expression, title=helper.nodefault, points=100, parser=mathtree.parser(), context={}):
3872 if title is helper.nodefault:
3873 self.title = expression
3874 else:
3875 self.title = title
3876 self.varname = varname
3877 self.min = min
3878 self.max = max
3879 self.points = points
3880 self.expression = {}
3881 self.mathtrees = {}
3882 varlist, expressionlist = expression.split("=")
3883 parsestr = mathtree.ParseStr(expressionlist)
3884 for key in varlist.split(","):
3885 key = key.strip()
3886 if self.mathtrees.has_key(key):
3887 raise ValueError("multiple assignment in tuple")
3888 try:
3889 self.mathtrees[key] = parser.ParseMathTree(parsestr)
3890 break
3891 except mathtree.CommaFoundMathTreeParseError, e:
3892 self.mathtrees[key] = e.MathTree
3893 else:
3894 raise ValueError("unpack tuple of wrong size")
3895 if len(varlist.split(",")) != len(self.mathtrees.keys()):
3896 raise ValueError("unpack tuple of wrong size")
3897 self.data = []
3898 for i in range(self.points):
3899 context[self.varname] = self.min + (self.max-self.min)*i / (self.points-1.0)
3900 line = []
3901 for key, tree in self.mathtrees.items():
3902 line.append(tree.Calc(**context))
3903 self.data.append(line)
3905 def setstyle(self, graph, style):
3906 self.style = style
3907 columns = {}
3908 for key, index in zip(self.mathtrees.keys(), xrange(sys.maxint)):
3909 columns[key] = index
3910 self.style.setcolumns(graph, columns)
3912 def getranges(self):
3913 return self.style.getranges(self.data)
3915 def setranges(self, ranges):
3916 pass
3918 def draw(self, graph):
3919 self.style.drawpoints(graph, self.data)