some fixes from tests under various python versions
[PyX/mjg.git] / pyx / graph.py
blobf3cfecbe8a0e514441497d866ad16db1985e089e
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 import re, math, string, sys
26 import bbox, box, canvas, color, deco, helper, path, style, unit, mathtree
27 import text as textmodule
28 import data as datamodule
29 import trafo as trafomodule
32 goldenmean = 0.5 * (math.sqrt(5) + 1)
35 ################################################################################
36 # maps
37 ################################################################################
39 class _Imap:
40 """interface definition of a map
41 maps convert a value into another value by bijective transformation f"""
43 def convert(self, x):
44 "returns f(x)"
46 def invert(self, y):
47 "returns f^-1(y) where f^-1 is the inverse transformation (x=f^-1(f(x)) for all x)"
49 def setbasepoints(self, basepoints):
50 """set basepoints for the convertions
51 basepoints are tuples (x, y) with y == f(x) and x == f^-1(y)
52 the number of basepoints needed might depend on the transformation
53 usually two pairs are needed like for linear maps, logarithmic maps, etc."""
56 class _linmap:
57 "linear mapping"
59 __implements__ = _Imap
61 def setbasepoints(self, basepoints):
62 self.dydx = (basepoints[1][1] - basepoints[0][1]) / float(basepoints[1][0] - basepoints[0][0])
63 self.dxdy = (basepoints[1][0] - basepoints[0][0]) / float(basepoints[1][1] - basepoints[0][1])
64 self.x1 = basepoints[0][0]
65 self.y1 = basepoints[0][1]
67 def convert(self, value):
68 return self.y1 + self.dydx * (value - self.x1)
70 def invert(self, value):
71 return self.x1 + self.dxdy * (value - self.y1)
74 class _logmap:
75 "logarithmic mapping"
76 __implements__ = _Imap
78 def setbasepoints(self, basepoints):
79 self.dydx = ((basepoints[1][1] - basepoints[0][1]) /
80 float(math.log(basepoints[1][0]) - math.log(basepoints[0][0])))
81 self.dxdy = ((math.log(basepoints[1][0]) - math.log(basepoints[0][0])) /
82 float(basepoints[1][1] - basepoints[0][1]))
83 self.x1 = math.log(basepoints[0][0])
84 self.y1 = basepoints[0][1]
85 return self
87 def convert(self, value):
88 return self.y1 + self.dydx * (math.log(value) - self.x1)
90 def invert(self, value):
91 return math.exp(self.x1 + self.dxdy * (value - self.y1))
95 ################################################################################
96 # partitioner
97 # please note the nomenclature:
98 # - a partition is a list of tick instances; to reduce name clashes, a
99 # partition is called ticks
100 # - a partitioner is a class creating a single or several ticks
101 # - an axis has a part attribute where it stores a partitioner or/and some
102 # (manually set) ticks -> the part attribute is used to create the ticks
103 # in the axis finish method
104 ################################################################################
107 class frac:
108 """fraction class for rational arithmetics
109 the axis partitioning uses rational arithmetics (with infinite accuracy)
110 basically it contains self.enum and self.denom"""
112 def stringfrac(self, s):
113 "converts a string 0.123 into a frac"
114 expparts = s.split("e")
115 if len(expparts) > 2:
116 raise ValueError("multiple 'e' found in '%s'" % s)
117 commaparts = expparts[0].split(".")
118 if len(commaparts) > 2:
119 raise ValueError("multiple '.' found in '%s'" % expparts[0])
120 if len(commaparts) == 1:
121 commaparts = [commaparts[0], ""]
122 result = frac((1, 10l), power=len(commaparts[1]))
123 neg = len(commaparts[0]) and commaparts[0][0] == "-"
124 if neg:
125 commaparts[0] = commaparts[0][1:]
126 elif len(commaparts[0]) and commaparts[0][0] == "+":
127 commaparts[0] = commaparts[0][1:]
128 if len(commaparts[0]):
129 if not commaparts[0].isdigit():
130 raise ValueError("unrecognized characters in '%s'" % s)
131 x = long(commaparts[0])
132 else:
133 x = 0
134 if len(commaparts[1]):
135 if not commaparts[1].isdigit():
136 raise ValueError("unrecognized characters in '%s'" % s)
137 y = long(commaparts[1])
138 else:
139 y = 0
140 result.enum = x*result.denom+y
141 if neg:
142 result.enum = -result.enum
143 if len(expparts) == 2:
144 neg = expparts[1][0] == "-"
145 if neg:
146 expparts[1] = expparts[1][1:]
147 elif expparts[1][0] == "+":
148 expparts[1] = expparts[1][1:]
149 if not expparts[1].isdigit():
150 raise ValueError("unrecognized characters in '%s'" % s)
151 if neg:
152 result *= frac((1, 10l), power=long(expparts[1]))
153 else:
154 result *= frac((10, 1l), power=long(expparts[1]))
155 return result
157 def floatfrac(self, x, floatprecision):
158 "converts a float into a frac with finite resolution"
159 if helper.isinteger(floatprecision) and floatprecision < 0:
160 # this would be extremly vulnerable
161 raise RuntimeError("float resolution must be non-negative integer")
162 return self.stringfrac(("%%.%ig" % floatprecision) % x)
164 def __init__(self, x, power=None, floatprecision=10):
165 "for power!=None: frac=(enum/denom)**power"
166 if helper.isnumber(x):
167 value = self.floatfrac(x, floatprecision)
168 enum, denom = value.enum, value.denom
169 elif helper.isstring(x):
170 fraction = x.split("/")
171 if len(fraction) > 2:
172 raise ValueError("multiple '/' found in '%s'" % x)
173 value = self.stringfrac(fraction[0])
174 if len(fraction) == 2:
175 value2 = self.stringfrac(fraction[1])
176 value = value / value2
177 enum, denom = value.enum, value.denom
178 else:
179 try:
180 enum, denom = x
181 except (TypeError, AttributeError):
182 enum, denom = x.enum, x.denom
183 if not helper.isinteger(enum) or not helper.isinteger(denom): raise TypeError("integer type expected")
184 if not denom: raise ZeroDivisionError("zero denominator")
185 if power != None:
186 if not helper.isinteger(power): raise TypeError("integer type expected")
187 if power >= 0:
188 self.enum = long(enum) ** power
189 self.denom = long(denom) ** power
190 else:
191 self.enum = long(denom) ** (-power)
192 self.denom = long(enum) ** (-power)
193 else:
194 self.enum = enum
195 self.denom = denom
197 def __cmp__(self, other):
198 if other is None:
199 return 1
200 return cmp(self.enum * other.denom, other.enum * self.denom)
202 def __abs__(self):
203 return frac((abs(self.enum), abs(self.denom)))
205 def __mul__(self, other):
206 return frac((self.enum * other.enum, self.denom * other.denom))
208 def __div__(self, other):
209 return frac((self.enum * other.denom, self.denom * other.enum))
211 def __float__(self):
212 "caution: avoid final precision of floats"
213 return float(self.enum) / self.denom
215 def __str__(self):
216 return "%i/%i" % (self.enum, self.denom)
219 class tick(frac):
220 """tick class
221 a tick is a frac enhanced by
222 - self.ticklevel (0 = tick, 1 = subtick, etc.)
223 - self.labellevel (0 = label, 1 = sublabel, etc.)
224 - self.label (a string) and self.labelattrs (a list, defaults to [])
225 When ticklevel or labellevel is None, no tick or label is present at that value.
226 When label is None, it should be automatically created (and stored), once the
227 an axis painter needs it. Classes, which implement _Itexter do precisely that."""
229 def __init__(self, pos, ticklevel=0, labellevel=0, label=None, labelattrs=[], **kwargs):
230 """initializes the instance
231 - see class description for the parameter description
232 - **kwargs are passed to the frac constructor"""
233 frac.__init__(self, pos, **kwargs)
234 self.ticklevel = ticklevel
235 self.labellevel = labellevel
236 self.label = label
237 self.labelattrs = helper.ensurelist(labelattrs)[:]
239 def merge(self, other):
240 """merges two ticks together:
241 - the lower ticklevel/labellevel wins
242 - the label is *never* taken over from other
243 - the ticks should be at the same position (otherwise it doesn't make sense)
244 -> this is NOT checked"""
245 if self.ticklevel is None or (other.ticklevel is not None and other.ticklevel < self.ticklevel):
246 self.ticklevel = other.ticklevel
247 if self.labellevel is None or (other.labellevel is not None and other.labellevel < self.labellevel):
248 self.labellevel = other.labellevel
251 def _mergeticklists(list1, list2):
252 """helper function to merge tick lists
253 - return a merged list of ticks out of list1 and list2
254 - CAUTION: original lists have to be ordered
255 (the returned list is also ordered)"""
256 # TODO: improve this using bisect?!
258 # XXX do not the original lists
259 list1 = list1[:]
260 i = 0
261 j = 0
262 try:
263 while 1: # we keep on going until we reach an index error
264 while list2[j] < list1[i]: # insert tick
265 list1.insert(i, list2[j])
266 i += 1
267 j += 1
268 if list2[j] == list1[i]: # merge tick
269 list1[i].merge(list2[j])
270 j += 1
271 i += 1
272 except IndexError:
273 if j < len(list2):
274 list1 += list2[j:]
275 return list1
278 def _mergelabels(ticks, labels):
279 """helper function to merge labels into ticks
280 - when labels is not None, the label of all ticks with
281 labellevel different from None are set
282 - labels need to be a list of lists of strings,
283 where the first list contain the strings to be
284 used as labels for the ticks with labellevel 0,
285 the second list for labellevel 1, etc.
286 - when the maximum labellevel is 0, just a list of
287 strings might be provided as the labels argument
288 - IndexError is raised, when a list length doesn't match"""
289 if helper.issequenceofsequences(labels):
290 for label, level in zip(labels, xrange(sys.maxint)):
291 usetext = helper.ensuresequence(label)
292 i = 0
293 for tick in ticks:
294 if tick.labellevel == level:
295 tick.label = usetext[i]
296 i += 1
297 if i != len(usetext):
298 raise IndexError("wrong list length of labels at level %i" % level)
299 elif labels is not None:
300 usetext = helper.ensuresequence(labels)
301 i = 0
302 for tick in ticks:
303 if tick.labellevel == 0:
304 tick.label = usetext[i]
305 i += 1
306 if i != len(usetext):
307 raise IndexError("wrong list length of labels")
310 class _Iparter:
311 """interface definition of a partition scheme
312 partition schemes are used to create a list of ticks"""
314 def defaultpart(self, min, max, extendmin, extendmax):
315 """create a partition
316 - returns an ordered list of ticks for the interval min to max
317 - the interval is given in float numbers, thus an appropriate
318 conversion to rational numbers has to be performed
319 - extendmin and extendmax are booleans (integers)
320 - when extendmin or extendmax is set, the ticks might
321 extend the min-max range towards lower and higher
322 ranges, respectively"""
324 def lesspart(self):
325 """create another partition which contains less ticks
326 - this method is called several times after a call of defaultpart
327 - returns an ordered list of ticks with less ticks compared to
328 the partition returned by defaultpart and by previous calls
329 of lesspart
330 - the creation of a partition with strictly *less* ticks
331 is not to be taken serious
332 - the method might return None, when no other appropriate
333 partition can be created"""
336 def morepart(self):
337 """create another partition which contains more ticks
338 see lesspart, but increase the number of ticks"""
341 class linparter:
342 """linear partition scheme
343 ticks and label distances are explicitly provided to the constructor"""
345 __implements__ = _Iparter
347 def __init__(self, tickdist=None, labeldist=None, labels=None, extendtick=0, extendlabel=None, epsilon=1e-10):
348 """configuration of the partition scheme
349 - tickdist and labeldist should be a list, where the first value
350 is the distance between ticks with ticklevel/labellevel 0,
351 the second list for ticklevel/labellevel 1, etc.;
352 a single entry is allowed without being a list
353 - tickdist and labeldist values are passed to the frac constructor
354 - when labeldist is None and tickdist is not None, the tick entries
355 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
356 - labels are applied to the resulting partition via the
357 mergelabels function (additional information available there)
358 - extendtick allows for the extension of the range given to the
359 defaultpart method to include the next tick with the specified
360 level (None turns off this feature); note, that this feature is
361 also disabled, when an axis prohibits its range extension by
362 the extendmin/extendmax variables given to the defaultpart method
363 - extendlabel is analogous to extendtick, but for labels
364 - epsilon allows for exceeding the axis range by this relative
365 value (relative to the axis range given to the defaultpart method)
366 without creating another tick specified by extendtick/extendlabel"""
367 if tickdist is None and labeldist is not None:
368 self.ticklist = (frac(helper.ensuresequence(labeldist)[0]),)
369 else:
370 self.ticklist = map(frac, helper.ensuresequence(tickdist))
371 if labeldist is None and tickdist is not None:
372 self.labellist = (frac(helper.ensuresequence(tickdist)[0]),)
373 else:
374 self.labellist = map(frac, helper.ensuresequence(labeldist))
375 self.labels = labels
376 self.extendtick = extendtick
377 self.extendlabel = extendlabel
378 self.epsilon = epsilon
380 def extendminmax(self, min, max, frac, extendmin, extendmax):
381 """return new min, max tuple extending the range min, max
382 - frac is the tick distance to be used
383 - extendmin and extendmax are booleans to allow for the extension"""
384 if extendmin:
385 min = float(frac) * math.floor(min / float(frac) + self.epsilon)
386 if extendmax:
387 max = float(frac) * math.ceil(max / float(frac) - self.epsilon)
388 return min, max
390 def getticks(self, min, max, frac, ticklevel=None, labellevel=None):
391 """return a list of equal spaced ticks
392 - the tick distance is frac, the ticklevel is set to ticklevel and
393 the labellevel is set to labellevel
394 - min, max is the range where ticks should be placed"""
395 imin = int(math.ceil(min / float(frac) - 0.5 * self.epsilon))
396 imax = int(math.floor(max / float(frac) + 0.5 * self.epsilon))
397 ticks = []
398 for i in range(imin, imax + 1):
399 ticks.append(tick((long(i) * frac.enum, frac.denom), ticklevel=ticklevel, labellevel=labellevel))
400 return ticks
402 def defaultpart(self, min, max, extendmin, extendmax):
403 if self.extendtick is not None and len(self.ticklist) > self.extendtick:
404 min, max = self.extendminmax(min, max, self.ticklist[self.extendtick], extendmin, extendmax)
405 if self.extendlabel is not None and len(self.labellist) > self.extendlabel:
406 min, max = self.extendminmax(min, max, self.labellist[self.extendlabel], extendmin, extendmax)
408 ticks = []
409 for i in range(len(self.ticklist)):
410 ticks = _mergeticklists(ticks, self.getticks(min, max, self.ticklist[i], ticklevel = i))
411 for i in range(len(self.labellist)):
412 ticks = _mergeticklists(ticks, self.getticks(min, max, self.labellist[i], labellevel = i))
414 _mergelabels(ticks, self.labels)
416 return ticks
418 def lesspart(self):
419 return None
421 def morepart(self):
422 return None
425 class autolinparter:
426 """automatic linear partition scheme
427 - possible tick distances are explicitly provided to the constructor
428 - tick distances are adjusted to the axis range by multiplication or division by 10"""
430 __implements__ = _Iparter
432 defaultvariants = ((frac((1, 1)), frac((1, 2))),
433 (frac((2, 1)), frac((1, 1))),
434 (frac((5, 2)), frac((5, 4))),
435 (frac((5, 1)), frac((5, 2))))
437 def __init__(self, variants=defaultvariants, extendtick=0, epsilon=1e-10):
438 """configuration of the partition scheme
439 - variants is a list of tickdist
440 - tickdist should be a list, where the first value
441 is the distance between ticks with ticklevel 0,
442 the second for ticklevel 1, etc.
443 - tickdist values are passed to the frac constructor
444 - labellevel is set to None except for those ticks in the partitions,
445 where ticklevel is zero. There labellevel is also set to zero.
446 - extendtick allows for the extension of the range given to the
447 defaultpart method to include the next tick with the specified
448 level (None turns off this feature); note, that this feature is
449 also disabled, when an axis prohibits its range extension by
450 the extendmin/extendmax variables given to the defaultpart method
451 - epsilon allows for exceeding the axis range by this relative
452 value (relative to the axis range given to the defaultpart method)
453 without creating another tick specified by extendtick"""
454 self.variants = variants
455 self.extendtick = extendtick
456 self.epsilon = epsilon
458 def defaultpart(self, min, max, extendmin, extendmax):
459 logmm = math.log(max - min) / math.log(10)
460 if logmm < 0: # correction for rounding towards zero of the int routine
461 base = frac((10L, 1), int(logmm - 1))
462 else:
463 base = frac((10L, 1), int(logmm))
464 ticks = map(frac, self.variants[0])
465 useticks = [tick * base for tick in ticks]
466 self.lesstickindex = self.moretickindex = 0
467 self.lessbase = frac((base.enum, base.denom))
468 self.morebase = frac((base.enum, base.denom))
469 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
470 part = linparter(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
471 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
473 def lesspart(self):
474 if self.lesstickindex < len(self.variants) - 1:
475 self.lesstickindex += 1
476 else:
477 self.lesstickindex = 0
478 self.lessbase.enum *= 10
479 ticks = map(frac, self.variants[self.lesstickindex])
480 useticks = [tick * self.lessbase for tick in ticks]
481 part = linparter(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
482 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
484 def morepart(self):
485 if self.moretickindex:
486 self.moretickindex -= 1
487 else:
488 self.moretickindex = len(self.variants) - 1
489 self.morebase.denom *= 10
490 ticks = map(frac, self.variants[self.moretickindex])
491 useticks = [tick * self.morebase for tick in ticks]
492 part = linparter(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
493 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
496 class preexp:
497 """storage class for the definition of logarithmic axes partitions
498 instances of this class define tick positions suitable for
499 logarithmic axes by the following instance variables:
500 - exp: integer, which defines multiplicator (usually 10)
501 - pres: list of tick positions (rational numbers, e.g. instances of frac)
502 possible positions are these tick positions and arbitrary divisions
503 and multiplications by the exp value"""
505 def __init__(self, pres, exp):
506 "create a preexp instance and store its pres and exp information"
507 self.pres = helper.ensuresequence(pres)
508 self.exp = exp
511 class logparter(linparter):
512 """logarithmic partition scheme
513 ticks and label positions are explicitly provided to the constructor"""
515 __implements__ = _Iparter
517 pre1exp5 = preexp(frac((1, 1)), 100000)
518 pre1exp4 = preexp(frac((1, 1)), 10000)
519 pre1exp3 = preexp(frac((1, 1)), 1000)
520 pre1exp2 = preexp(frac((1, 1)), 100)
521 pre1exp = preexp(frac((1, 1)), 10)
522 pre125exp = preexp((frac((1, 1)), frac((2, 1)), frac((5, 1))), 10)
523 pre1to9exp = preexp(map(lambda x: frac((x, 1)), range(1, 10)), 10)
524 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
526 def __init__(self, tickpos=None, labelpos=None, labels=None, extendtick=0, extendlabel=None, epsilon=1e-10):
527 """configuration of the partition scheme
528 - tickpos and labelpos should be a list, where the first entry
529 is a preexp instance describing ticks with ticklevel/labellevel 0,
530 the second is a preexp instance for ticklevel/labellevel 1, etc.;
531 a single entry is allowed without being a list
532 - when labelpos is None and tickpos is not None, the tick entries
533 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
534 - labels are applied to the resulting partition via the
535 mergetexts function (additional information available there)
536 - extendtick allows for the extension of the range given to the
537 defaultpart method to include the next tick with the specified
538 level (None turns off this feature); note, that this feature is
539 also disabled, when an axis prohibits its range extension by
540 the extendmin/extendmax variables given to the defaultpart method
541 - extendlabel is analogous to extendtick, but for labels
542 - epsilon allows for exceeding the axis range by this relative
543 logarithm value (relative to the logarithm axis range given
544 to the defaultpart method) without creating another tick
545 specified by extendtick/extendlabel"""
546 if tickpos is None and labels is not None:
547 self.ticklist = (helper.ensuresequence(labelpos)[0],)
548 else:
549 self.ticklist = helper.ensuresequence(tickpos)
551 if labelpos is None and tickpos is not None:
552 self.labellist = (helper.ensuresequence(tickpos)[0],)
553 else:
554 self.labellist = helper.ensuresequence(labelpos)
555 self.labels = labels
556 self.extendtick = extendtick
557 self.extendlabel = extendlabel
558 self.epsilon = epsilon
560 def extendminmax(self, min, max, preexp, extendmin, extendmax):
561 """return new min, max tuple extending the range min, max
562 preexp describes the allowed tick positions
563 extendmin and extendmax are booleans to allow for the extension"""
564 minpower = None
565 maxpower = None
566 for i in xrange(len(preexp.pres)):
567 imin = int(math.floor(math.log(min / float(preexp.pres[i])) /
568 math.log(preexp.exp) + self.epsilon)) + 1
569 imax = int(math.ceil(math.log(max / float(preexp.pres[i])) /
570 math.log(preexp.exp) - self.epsilon)) - 1
571 if minpower is None or imin < minpower:
572 minpower, minindex = imin, i
573 if maxpower is None or imax >= maxpower:
574 maxpower, maxindex = imax, i
575 if minindex:
576 minfrac = preexp.pres[minindex - 1]
577 else:
578 minfrac = preexp.pres[-1]
579 minpower -= 1
580 if maxindex != len(preexp.pres) - 1:
581 maxfrac = preexp.pres[maxindex + 1]
582 else:
583 maxfrac = preexp.pres[0]
584 maxpower += 1
585 if extendmin:
586 min = float(minfrac) * float(preexp.exp) ** minpower
587 if extendmax:
588 max = float(maxfrac) * float(preexp.exp) ** maxpower
589 return min, max
591 def getticks(self, min, max, preexp, ticklevel=None, labellevel=None):
592 """return a list of ticks
593 - preexp describes the allowed tick positions
594 - the ticklevel of the ticks is set to ticklevel and
595 the labellevel is set to labellevel
596 - min, max is the range where ticks should be placed"""
597 ticks = []
598 minimin = 0
599 maximax = 0
600 for f in preexp.pres:
601 fracticks = []
602 imin = int(math.ceil(math.log(min / float(f)) /
603 math.log(preexp.exp) - 0.5 * self.epsilon))
604 imax = int(math.floor(math.log(max / float(f)) /
605 math.log(preexp.exp) + 0.5 * self.epsilon))
606 for i in range(imin, imax + 1):
607 pos = f * frac((preexp.exp, 1), i)
608 fracticks.append(tick((pos.enum, pos.denom), ticklevel = ticklevel, labellevel = labellevel))
609 ticks = _mergeticklists(ticks, fracticks)
610 return ticks
613 class autologparter(logparter):
614 """automatic logarithmic partition scheme
615 possible tick positions are explicitly provided to the constructor"""
617 __implements__ = _Iparter
619 defaultvariants = (((logparter.pre1exp, # ticks
620 logparter.pre1to9exp), # subticks
621 (logparter.pre1exp, # labels
622 logparter.pre125exp)), # sublevels
624 ((logparter.pre1exp, # ticks
625 logparter.pre1to9exp), # subticks
626 None), # labels like ticks
628 ((logparter.pre1exp2, # ticks
629 logparter.pre1exp), # subticks
630 None), # labels like ticks
632 ((logparter.pre1exp3, # ticks
633 logparter.pre1exp), # subticks
634 None), # labels like ticks
636 ((logparter.pre1exp4, # ticks
637 logparter.pre1exp), # subticks
638 None), # labels like ticks
640 ((logparter.pre1exp5, # ticks
641 logparter.pre1exp), # subticks
642 None)) # labels like ticks
644 def __init__(self, variants=defaultvariants, extendtick=0, extendlabel=None, epsilon=1e-10):
645 """configuration of the partition scheme
646 - variants should be a list of pairs of lists of preexp
647 instances
648 - within each pair the first list contains preexp, where
649 the first preexp instance describes ticks positions with
650 ticklevel 0, the second preexp for ticklevel 1, etc.
651 - the second list within each pair describes the same as
652 before, but for labels
653 - within each pair: when the second entry (for the labels) is None
654 and the first entry (for the ticks) ticks is not None, the tick
655 entries for ticklevel 0 are used for labels and vice versa
656 (ticks<->labels)
657 - extendtick allows for the extension of the range given to the
658 defaultpart method to include the next tick with the specified
659 level (None turns off this feature); note, that this feature is
660 also disabled, when an axis prohibits its range extension by
661 the extendmin/extendmax variables given to the defaultpart method
662 - extendlabel is analogous to extendtick, but for labels
663 - epsilon allows for exceeding the axis range by this relative
664 logarithm value (relative to the logarithm axis range given
665 to the defaultpart method) without creating another tick
666 specified by extendtick/extendlabel"""
667 self.variants = variants
668 if len(variants) > 2:
669 self.variantsindex = divmod(len(variants), 2)[0]
670 else:
671 self.variantsindex = 0
672 self.extendtick = extendtick
673 self.extendlabel = extendlabel
674 self.epsilon = epsilon
676 def defaultpart(self, min, max, extendmin, extendmax):
677 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
678 self.morevariantsindex = self.variantsindex
679 self.lessvariantsindex = self.variantsindex
680 part = logparter(tickpos=self.variants[self.variantsindex][0], labelpos=self.variants[self.variantsindex][1],
681 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
682 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
684 def lesspart(self):
685 self.lessvariantsindex += 1
686 if self.lessvariantsindex < len(self.variants):
687 part = logparter(tickpos=self.variants[self.lessvariantsindex][0], labelpos=self.variants[self.lessvariantsindex][1],
688 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
689 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
691 def morepart(self):
692 self.morevariantsindex -= 1
693 if self.morevariantsindex >= 0:
694 part = logparter(tickpos=self.variants[self.morevariantsindex][0], labelpos=self.variants[self.morevariantsindex][1],
695 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
696 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
700 ################################################################################
701 # rater
702 # conseptional remarks:
703 # - raters are used to calculate a rating for a realization of something
704 # - here, a rating means a positive floating point value
705 # - ratings are used to order those realizations by their suitability (lower
706 # ratings are better)
707 # - a rating of None means not suitable at all (those realizations should be
708 # thrown out)
709 ################################################################################
712 class cuberater:
713 """a value rater
714 - a cube rater has an optimal value, where the rate becomes zero
715 - for a left (below the optimum) and a right value (above the optimum),
716 the rating is value is set to 1 (modified by an overall weight factor
717 for the rating)
718 - the analytic form of the rating is cubic for both, the left and
719 the right side of the rater, independently"""
721 # __implements__ = sole implementation
723 def __init__(self, opt, left=None, right=None, weight=1):
724 """initializes the rater
725 - by default, left is set to zero, right is set to 3*opt
726 - left should be smaller than opt, right should be bigger than opt
727 - weight should be positive and is a factor multiplicated to the rates"""
728 if left is None:
729 left = 0
730 if right is None:
731 right = 3*opt
732 self.opt = opt
733 self.left = left
734 self.right = right
735 self.weight = weight
737 def rate(self, value, density):
738 """returns a rating for a value
739 - the density lineary rescales the rater (the optimum etc.),
740 e.g. a value bigger than one increases the optimum (when it is
741 positive) and a value lower than one decreases the optimum (when
742 it is positive); the density itself should be positive"""
743 opt = self.opt * density
744 if value < opt:
745 other = self.left * density
746 elif value > opt:
747 other = self.right * density
748 else:
749 return 0
750 factor = (value - opt) / float(other - opt)
751 return self.weight * (factor ** 3)
754 class distancerater:
755 # TODO: update docstring
756 """a distance rater (rates a list of distances)
757 - the distance rater rates a list of distances by rating each independently
758 and returning the average rate
759 - there is an optimal value, where the rate becomes zero
760 - the analytic form is linary for values above the optimal value
761 (twice the optimal value has the rating one, three times the optimal
762 value has the rating two, etc.)
763 - the analytic form is reciprocal subtracting one for values below the
764 optimal value (halve the optimal value has the rating one, one third of
765 the optimal value has the rating two, etc.)"""
767 # __implements__ = sole implementation
769 def __init__(self, opt, weight=0.1):
770 """inititializes the rater
771 - opt is the optimal length (a visual PyX length)
772 - weight should be positive and is a factor multiplicated to the rates"""
773 self.opt_str = opt
774 self.weight = weight
776 def rate(self, distances, density):
777 """rate distances
778 - the distances are a list of positive floats in PostScript points
779 - the density lineary rescales the rater (the optimum etc.),
780 e.g. a value bigger than one increases the optimum (when it is
781 positive) and a value lower than one decreases the optimum (when
782 it is positive); the density itself should be positive"""
783 if len(distances):
784 opt = unit.topt(unit.length(self.opt_str, default_type="v")) / density
785 rate = 0
786 for distance in distances:
787 if distance < opt:
788 rate += self.weight * (opt / distance - 1)
789 else:
790 rate += self.weight * (distance / opt - 1)
791 return rate / float(len(distances))
794 class axisrater:
795 """a rater for ticks
796 - the rating of axes is splited into two separate parts:
797 - rating of the ticks in terms of the number of ticks, subticks,
798 labels, etc.
799 - rating of the label distances
800 - in the end, a rate for ticks is the sum of these rates
801 - it is useful to first just rate the number of ticks etc.
802 and selecting those partitions, where this fits well -> as soon
803 as an complete rate (the sum of both parts from the list above)
804 of a first ticks is below a rate of just the number of ticks,
805 subticks labels etc. of other ticks, those other ticks will never
806 be better than the first one -> we gain speed by minimizing the
807 number of ticks, where label distances have to be taken into account)
808 - both parts of the rating are shifted into instances of raters
809 defined above --- right now, there is not yet a strict interface
810 for this delegation (should be done as soon as it is needed)"""
812 # __implements__ = sole implementation
814 linticks = (cuberater(4), cuberater(10, weight=0.5), )
815 linlabels = (cuberater(4), )
816 logticks = (cuberater(5, right=20), cuberater(20, right=100, weight=0.5), )
817 loglabels = (cuberater(5, right=20), cuberater(5, left=-20, right=20, weight=0.5), )
818 stdrange = cuberater(1, weight=2)
819 stddistance = distancerater("1 cm")
821 def __init__(self, ticks=linticks, labels=linlabels, range=stdrange, distance=stddistance):
822 """initializes the axis rater
823 - ticks and labels are lists of instances of a value rater
824 - the first entry in ticks rate the number of ticks, the
825 second the number of subticks, etc.; when there are no
826 ticks of a level or there is not rater for a level, the
827 level is just ignored
828 - labels is analogous, but for labels
829 - within the rating, all ticks with a higher level are
830 considered as ticks for a given level
831 - range is a value rater instance, which rates the covering
832 of an axis range by the ticks (as a relative value of the
833 tick range vs. the axis range), ticks might cover less or
834 more than the axis range (for the standard automatic axis
835 partition schemes an extention of the axis range is normal
836 and should get some penalty)
837 - distance is an distance rater instance"""
838 self.ticks = ticks
839 self.labels = labels
840 self.range = range
841 self.distance = distance
843 def rateticks(self, axis, ticks, density):
844 """rates ticks by the number of ticks, subticks, labels etc.
845 - takes into account the number of ticks, subticks, labels
846 etc. and the coverage of the axis range by the ticks
847 - when there are no ticks of a level or there was not rater
848 given in the constructor for a level, the level is just
849 ignored
850 - the method returns the sum of the rating results divided
851 by the sum of the weights of the raters
852 - within the rating, all ticks with a higher level are
853 considered as ticks for a given level"""
854 maxticklevel = maxlabellevel = 0
855 for tick in ticks:
856 if tick.ticklevel is not None and tick.ticklevel >= maxticklevel:
857 maxticklevel = tick.ticklevel + 1
858 if tick.labellevel is not None and tick.labellevel >= maxlabellevel:
859 maxlabellevel = tick.labellevel + 1
860 numticks = [0]*maxticklevel
861 numlabels = [0]*maxlabellevel
862 for tick in ticks:
863 if tick.ticklevel is not None:
864 for level in range(tick.ticklevel, maxticklevel):
865 numticks[level] += 1
866 if tick.labellevel is not None:
867 for level in range(tick.labellevel, maxlabellevel):
868 numlabels[level] += 1
869 rate = 0
870 weight = 0
871 for numtick, rater in zip(numticks, self.ticks):
872 rate += rater.rate(numtick, density)
873 weight += rater.weight
874 for numlabel, rater in zip(numlabels, self.labels):
875 rate += rater.rate(numlabel, density)
876 weight += rater.weight
877 return rate/weight
879 def raterange(self, tickrange, datarange):
880 """rate the range covered by the ticks compared to the range
881 of the data
882 - tickrange and datarange are the ranges covered by the ticks
883 and the data in graph coordinates
884 - usually, the datarange is 1 (ticks are calculated for a
885 given datarange)
886 - the ticks might cover less or more than the data range (for
887 the standard automatic axis partition schemes an extention
888 of the axis range is normal and should get some penalty)"""
889 return self.range.rate(tickrange, datarange)
891 def ratelayout(self, axiscanvas, density):
892 """rate distances of the labels in an axis canvas
893 - the distances should be collected as box distances of
894 subsequent labels
895 - the axiscanvas provides a labels attribute for easy
896 access to the labels whose distances have to be taken
897 into account
898 - the density is used within the distancerate instance"""
899 if len(axiscanvas.labels) > 1:
900 try:
901 distances = [axiscanvas.labels[i].boxdistance_pt(axiscanvas.labels[i+1]) for i in range(len(axiscanvas.labels) - 1)]
902 except box.BoxCrossError:
903 return None
904 return self.distance.rate(distances, density)
905 else:
906 return None
909 ################################################################################
910 # texter
911 # texter automatically create labels for tick instances
912 ################################################################################
915 class _Itexter:
917 def labels(self, ticks):
918 """fill the label attribute of ticks
919 - ticks is a list of instances of tick
920 - for each element of ticks the value of the attribute label is set to
921 a string appropriate to the attributes enum and denom of that tick
922 instance
923 - label attributes of the tick instances are just kept, whenever they
924 are not equal to None
925 - the method might extend the labelattrs attribute of the ticks"""
928 class rationaltexter:
929 "a texter creating rational labels (e.g. 'a/b' or even 'a \over b')"
930 # XXX: we use divmod here to be more expicit
932 __implements__ = _Itexter
934 def __init__(self, prefix="", infix="", suffix="",
935 enumprefix="", enuminfix="", enumsuffix="",
936 denomprefix="", denominfix="", denomsuffix="",
937 plus="", minus="-", minuspos=0, over=r"{{%s}\over{%s}}",
938 equaldenom=0, skip1=1, skipenum0=1, skipenum1=1, skipdenom1=1,
939 labelattrs=textmodule.mathmode):
940 r"""initializes the instance
941 - prefix, infix, and suffix (strings) are added at the begin,
942 immediately after the minus, and at the end of the label,
943 respectively
944 - prefixenum, infixenum, and suffixenum (strings) are added
945 to the labels enumerator correspondingly
946 - prefixdenom, infixdenom, and suffixdenom (strings) are added
947 to the labels denominator correspondingly
948 - plus or minus (string) is inserted for non-negative or negative numbers
949 - minuspos is an integer, which determines the position, where the
950 plus or minus sign has to be placed; the following values are allowed:
951 1 - writes the plus or minus in front of the enumerator
952 0 - writes the plus or minus in front of the hole fraction
953 -1 - writes the plus or minus in front of the denominator
954 - over (string) is taken as a format string generating the
955 fraction bar; it has to contain exactly two string insert
956 operators "%s" -- the first for the enumerator and the second
957 for the denominator; by far the most common examples are
958 r"{{%s}\over{%s}}" and "{{%s}/{%s}}"
959 - usually the enumerator and denominator are canceled; however,
960 when equaldenom is set, the least common multiple of all
961 denominators is used
962 - skip1 (boolean) just prints the prefix, the plus or minus,
963 the infix and the suffix, when the value is plus or minus one
964 and at least one of prefix, infix and the suffix is present
965 - skipenum0 (boolean) just prints a zero instead of
966 the hole fraction, when the enumerator is zero;
967 no prefixes, infixes, and suffixes are taken into account
968 - skipenum1 (boolean) just prints the enumprefix, the plus or minus,
969 the enuminfix and the enumsuffix, when the enum value is plus or minus one
970 and at least one of enumprefix, enuminfix and the enumsuffix is present
971 - skipdenom1 (boolean) just prints the enumerator instead of
972 the hole fraction, when the denominator is one and none of the parameters
973 denomprefix, denominfix and denomsuffix are set and minuspos is not -1 or the
974 fraction is positive
975 - labelattrs is a list of attributes for a texrunners text method;
976 a single is allowed without being a list; None is considered as
977 an empty list"""
978 self.prefix = prefix
979 self.infix = infix
980 self.suffix = suffix
981 self.enumprefix = enumprefix
982 self.enuminfix = enuminfix
983 self.enumsuffix = enumsuffix
984 self.denomprefix = denomprefix
985 self.denominfix = denominfix
986 self.denomsuffix = denomsuffix
987 self.plus = plus
988 self.minus = minus
989 self.minuspos = minuspos
990 self.over = over
991 self.equaldenom = equaldenom
992 self.skip1 = skip1
993 self.skipenum0 = skipenum0
994 self.skipenum1 = skipenum1
995 self.skipdenom1 = skipdenom1
996 self.labelattrs = helper.ensurelist(labelattrs)
998 def gcd(self, *n):
999 """returns the greates common divisor of all elements in n
1000 - the elements of n must be non-negative integers
1001 - return None if the number of elements is zero
1002 - the greates common divisor is not affected when some
1003 of the elements are zero, but it becomes zero when
1004 all elements are zero"""
1005 if len(n) == 2:
1006 i, j = n
1007 if i < j:
1008 i, j = j, i
1009 while j > 0:
1010 i, (dummy, j) = j, divmod(i, j)
1011 return i
1012 if len(n):
1013 res = n[0]
1014 for i in n[1:]:
1015 res = self.gcd(res, i)
1016 return res
1018 def lcm(self, *n):
1019 """returns the least common multiple of all elements in n
1020 - the elements of n must be non-negative integers
1021 - return None if the number of elements is zero
1022 - the least common multiple is zero when some of the
1023 elements are zero"""
1024 if len(n):
1025 res = n[0]
1026 for i in n[1:]:
1027 res = divmod(res * i, self.gcd(res, i))[0]
1028 return res
1030 def labels(self, ticks):
1031 labeledticks = []
1032 for tick in ticks:
1033 if tick.label is None and tick.labellevel is not None:
1034 labeledticks.append(tick)
1035 tick.temp_fracenum = tick.enum
1036 tick.temp_fracdenom = tick.denom
1037 tick.temp_fracminus = 1
1038 if tick.temp_fracenum < 0:
1039 tick.temp_fracminus = -tick.temp_fracminus
1040 tick.temp_fracenum = -tick.temp_fracenum
1041 if tick.temp_fracdenom < 0:
1042 tick.temp_fracminus = -tick.temp_fracminus
1043 tick.temp_fracdenom = -tick.temp_fracdenom
1044 gcd = self.gcd(tick.temp_fracenum, tick.temp_fracdenom)
1045 (tick.temp_fracenum, dummy1), (tick.temp_fracdenom, dummy2) = divmod(tick.temp_fracenum, gcd), divmod(tick.temp_fracdenom, gcd)
1046 if self.equaldenom:
1047 equaldenom = self.lcm(*[tick.temp_fracdenom for tick in ticks if tick.label is None])
1048 if equaldenom is not None:
1049 for tick in labeledticks:
1050 factor, dummy = divmod(equaldenom, tick.temp_fracdenom)
1051 tick.temp_fracenum, tick.temp_fracdenom = factor * tick.temp_fracenum, factor * tick.temp_fracdenom
1052 for tick in labeledticks:
1053 fracminus = fracenumminus = fracdenomminus = ""
1054 if tick.temp_fracminus == -1:
1055 plusminus = self.minus
1056 else:
1057 plusminus = self.plus
1058 if self.minuspos == 0:
1059 fracminus = plusminus
1060 elif self.minuspos == 1:
1061 fracenumminus = plusminus
1062 elif self.minuspos == -1:
1063 fracdenomminus = plusminus
1064 else:
1065 raise RuntimeError("invalid minuspos")
1066 if self.skipenum0 and tick.temp_fracenum == 0:
1067 tick.label = "0"
1068 elif (self.skip1 and self.skipdenom1 and tick.temp_fracenum == 1 and tick.temp_fracdenom == 1 and
1069 (len(self.prefix) or len(self.infix) or len(self.suffix)) and
1070 not len(fracenumminus) and not len(self.enumprefix) and not len(self.enuminfix) and not len(self.enumsuffix) and
1071 not len(fracdenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix)):
1072 tick.label = "%s%s%s%s" % (self.prefix, fracminus, self.infix, self.suffix)
1073 else:
1074 if self.skipenum1 and tick.temp_fracenum == 1 and (len(self.enumprefix) or len(self.enuminfix) or len(self.enumsuffix)):
1075 tick.temp_fracenum = "%s%s%s%s" % (self.enumprefix, fracenumminus, self.enuminfix, self.enumsuffix)
1076 else:
1077 tick.temp_fracenum = "%s%s%s%i%s" % (self.enumprefix, fracenumminus, self.enuminfix, tick.temp_fracenum, self.enumsuffix)
1078 if self.skipdenom1 and tick.temp_fracdenom == 1 and not len(fracdenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix):
1079 frac = tick.temp_fracenum
1080 else:
1081 tick.temp_fracdenom = "%s%s%s%i%s" % (self.denomprefix, fracdenomminus, self.denominfix, tick.temp_fracdenom, self.denomsuffix)
1082 frac = self.over % (tick.temp_fracenum, tick.temp_fracdenom)
1083 tick.label = "%s%s%s%s%s" % (self.prefix, fracminus, self.infix, frac, self.suffix)
1084 tick.labelattrs.extend(self.labelattrs)
1086 # del tick.temp_fracenum # we've inserted those temporary variables ... and do not care any longer about them
1087 # del tick.temp_fracdenom
1088 # del tick.temp_fracminus
1092 class decimaltexter:
1093 "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')"
1095 __implements__ = _Itexter
1097 def __init__(self, prefix="", infix="", suffix="", equalprecision=0,
1098 decimalsep=".", thousandsep="", thousandthpartsep="",
1099 plus="", minus="-", period=r"\overline{%s}", labelattrs=textmodule.mathmode):
1100 r"""initializes the instance
1101 - prefix, infix, and suffix (strings) are added at the begin,
1102 immediately after the minus, and at the end of the label,
1103 respectively
1104 - decimalsep, thousandsep, and thousandthpartsep (strings)
1105 are used as separators
1106 - plus or minus (string) is inserted for non-negative or negative numbers
1107 - period (string) is taken as a format string generating a period;
1108 it has to contain exactly one string insert operators "%s" for the
1109 period; usually it should be r"\overline{%s}"
1110 - labelattrs is a list of attributes for a texrunners text method;
1111 a single is allowed without being a list; None is considered as
1112 an empty list"""
1113 self.prefix = prefix
1114 self.infix = infix
1115 self.suffix = suffix
1116 self.equalprecision = equalprecision
1117 self.decimalsep = decimalsep
1118 self.thousandsep = thousandsep
1119 self.thousandthpartsep = thousandthpartsep
1120 self.plus = plus
1121 self.minus = minus
1122 self.period = period
1123 self.labelattrs = helper.ensurelist(labelattrs)
1125 def labels(self, ticks):
1126 labeledticks = []
1127 maxdecprecision = 0
1128 for tick in ticks:
1129 if tick.label is None and tick.labellevel is not None:
1130 labeledticks.append(tick)
1131 m, n = tick.enum, tick.denom
1132 if m < 0: m = -m
1133 if n < 0: n = -n
1134 whole, reminder = divmod(m, n)
1135 whole = str(whole)
1136 if len(self.thousandsep):
1137 l = len(whole)
1138 tick.label = ""
1139 for i in range(l):
1140 tick.label += whole[i]
1141 if not ((l-i-1) % 3) and l > i+1:
1142 tick.label += self.thousandsep
1143 else:
1144 tick.label = whole
1145 if reminder:
1146 tick.label += self.decimalsep
1147 oldreminders = []
1148 tick.temp_decprecision = 0
1149 while (reminder):
1150 tick.temp_decprecision += 1
1151 if reminder in oldreminders:
1152 tick.temp_decprecision = None
1153 periodstart = len(tick.label) - (len(oldreminders) - oldreminders.index(reminder))
1154 tick.label = tick.label[:periodstart] + self.period % tick.label[periodstart:]
1155 break
1156 oldreminders += [reminder]
1157 reminder *= 10
1158 whole, reminder = divmod(reminder, n)
1159 if not ((tick.temp_decprecision - 1) % 3) and tick.temp_decprecision > 1:
1160 tick.label += self.thousandthpartsep
1161 tick.label += str(whole)
1162 if maxdecprecision < tick.temp_decprecision:
1163 maxdecprecision = tick.temp_decprecision
1164 if self.equalprecision:
1165 for tick in labeledticks:
1166 if tick.temp_decprecision is not None:
1167 if tick.temp_decprecision == 0 and maxdecprecision > 0:
1168 tick.label += self.decimalsep
1169 for i in range(tick.temp_decprecision, maxdecprecision):
1170 if not ((i - 1) % 3) and i > 1:
1171 tick.label += self.thousandthpartsep
1172 tick.label += "0"
1173 for tick in labeledticks:
1174 if tick.enum * tick.denom < 0:
1175 plusminus = self.minus
1176 else:
1177 plusminus = self.plus
1178 tick.label = "%s%s%s%s%s" % (self.prefix, plusminus, self.infix, tick.label, self.suffix)
1179 tick.labelattrs.extend(self.labelattrs)
1181 # del tick.temp_decprecision # we've inserted this temporary variable ... and do not care any longer about it
1184 class exponentialtexter:
1185 "a texter creating labels with exponentials (e.g. '2\cdot10^5')"
1187 __implements__ = _Itexter
1189 def __init__(self, plus="", minus="-",
1190 mantissaexp=r"{{%s}\cdot10^{%s}}",
1191 skipexp0=r"{%s}",
1192 skipexp1=None,
1193 nomantissaexp=r"{10^{%s}}",
1194 minusnomantissaexp=r"{-10^{%s}}",
1195 mantissamin=frac((1, 1)), mantissamax=frac((10, 1)),
1196 skipmantissa1=0, skipallmantissa1=1,
1197 mantissatexter=decimaltexter()):
1198 r"""initializes the instance
1199 - plus or minus (string) is inserted for non-negative or negative exponents
1200 - mantissaexp (string) is taken as a format string generating the exponent;
1201 it has to contain exactly two string insert operators "%s" --
1202 the first for the mantissa and the second for the exponent;
1203 examples are r"{{%s}\cdot10^{%s}}" and r"{{%s}{\rm e}{%s}}"
1204 - skipexp0 (string) is taken as a format string used for exponent 0;
1205 exactly one string insert operators "%s" for the mantissa;
1206 None turns off the special handling of exponent 0;
1207 an example is r"{%s}"
1208 - skipexp1 (string) is taken as a format string used for exponent 1;
1209 exactly one string insert operators "%s" for the mantissa;
1210 None turns off the special handling of exponent 1;
1211 an example is r"{{%s}\cdot10}"
1212 - nomantissaexp (string) is taken as a format string generating the exponent
1213 when the mantissa is one and should be skipped; it has to contain
1214 exactly one string insert operators "%s" for the exponent;
1215 an examples is r"{10^{%s}}"
1216 - minusnomantissaexp (string) is taken as a format string generating the exponent
1217 when the mantissa is minus one and should be skipped; it has to contain
1218 exactly one string insert operators "%s" for the exponent;
1219 None turns off the special handling of mantissa -1;
1220 an examples is r"{-10^{%s}}"
1221 - mantissamin and mantissamax are the minimum and maximum of the mantissa;
1222 they are frac instances greater than zero and mantissamin < mantissamax;
1223 the sign of the tick is ignored here
1224 - skipmantissa1 (boolean) turns on skipping of any mantissa equals one
1225 (and minus when minusnomantissaexp is set)
1226 - skipallmantissa1 (boolean) as above, but all mantissas must be 1 (or -1)
1227 - mantissatexter is the texter for the mantissa
1228 - the skipping of a mantissa is stronger than the skipping of an exponent"""
1229 self.plus = plus
1230 self.minus = minus
1231 self.mantissaexp = mantissaexp
1232 self.skipexp0 = skipexp0
1233 self.skipexp1 = skipexp1
1234 self.nomantissaexp = nomantissaexp
1235 self.minusnomantissaexp = minusnomantissaexp
1236 self.mantissamin = mantissamin
1237 self.mantissamax = mantissamax
1238 self.mantissamindivmax = self.mantissamin / self.mantissamax
1239 self.mantissamaxdivmin = self.mantissamax / self.mantissamin
1240 self.skipmantissa1 = skipmantissa1
1241 self.skipallmantissa1 = skipallmantissa1
1242 self.mantissatexter = mantissatexter
1244 def labels(self, ticks):
1245 labeledticks = []
1246 for tick in ticks:
1247 if tick.label is None and tick.labellevel is not None:
1248 tick.temp_orgenum, tick.temp_orgdenom = tick.enum, tick.denom
1249 labeledticks.append(tick)
1250 tick.temp_exp = 0
1251 if tick.enum:
1252 while abs(tick) >= self.mantissamax:
1253 tick.temp_exp += 1
1254 x = tick * self.mantissamindivmax
1255 tick.enum, tick.denom = x.enum, x.denom
1256 while abs(tick) < self.mantissamin:
1257 tick.temp_exp -= 1
1258 x = tick * self.mantissamaxdivmin
1259 tick.enum, tick.denom = x.enum, x.denom
1260 if tick.temp_exp < 0:
1261 tick.temp_exp = "%s%i" % (self.minus, -tick.temp_exp)
1262 else:
1263 tick.temp_exp = "%s%i" % (self.plus, tick.temp_exp)
1264 self.mantissatexter.labels(labeledticks)
1265 if self.minusnomantissaexp is not None:
1266 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if abs(tick.enum) == abs(tick.denom)])
1267 else:
1268 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if tick.enum == tick.denom])
1269 for tick in labeledticks:
1270 if (self.skipallmantissa1 and allmantissa1 or
1271 (self.skipmantissa1 and (tick.enum == tick.denom or
1272 (tick.enum == -tick.denom and self.minusnomantissaexp is not None)))):
1273 if tick.enum == tick.denom:
1274 tick.label = self.nomantissaexp % tick.temp_exp
1275 else:
1276 tick.label = self.minusnomantissaexp % tick.temp_exp
1277 else:
1278 if tick.temp_exp == "0" and self.skipexp0 is not None:
1279 tick.label = self.skipexp0 % tick.label
1280 elif tick.temp_exp == "1" and self.skipexp1 is not None:
1281 tick.label = self.skipexp1 % tick.label
1282 else:
1283 tick.label = self.mantissaexp % (tick.label, tick.temp_exp)
1284 tick.enum, tick.denom = tick.temp_orgenum, tick.temp_orgdenom
1286 # del tick.temp_orgenum # we've inserted those temporary variables ... and do not care any longer about them
1287 # del tick.temp_orgdenom
1288 # del tick.temp_exp
1291 class defaulttexter:
1292 "a texter creating decimal or exponential labels"
1294 __implements__ = _Itexter
1296 def __init__(self, smallestdecimal=frac((1, 1000)),
1297 biggestdecimal=frac((9999, 1)),
1298 equaldecision=1,
1299 decimaltexter=decimaltexter(),
1300 exponentialtexter=exponentialtexter()):
1301 r"""initializes the instance
1302 - smallestdecimal and biggestdecimal are the smallest and
1303 biggest decimal values, where the decimaltexter should be used;
1304 they are frac instances; the sign of the tick is ignored here;
1305 a tick at zero is considered for the decimaltexter as well
1306 - equaldecision (boolean) uses decimaltexter or exponentialtexter
1307 globaly (set) or for each tick separately (unset)
1308 - decimaltexter and exponentialtexter are texters to be used"""
1309 self.smallestdecimal = smallestdecimal
1310 self.biggestdecimal = biggestdecimal
1311 self.equaldecision = equaldecision
1312 self.decimaltexter = decimaltexter
1313 self.exponentialtexter = exponentialtexter
1315 def labels(self, ticks):
1316 decticks = []
1317 expticks = []
1318 for tick in ticks:
1319 if tick.label is None and tick.labellevel is not None:
1320 if not tick.enum or (abs(tick) >= self.smallestdecimal and abs(tick) <= self.biggestdecimal):
1321 decticks.append(tick)
1322 else:
1323 expticks.append(tick)
1324 if self.equaldecision:
1325 if len(expticks):
1326 self.exponentialtexter.labels(ticks)
1327 else:
1328 self.decimaltexter.labels(ticks)
1329 else:
1330 for tick in decticks:
1331 self.decimaltexter.labels([tick])
1332 for tick in expticks:
1333 self.exponentialtexter.labels([tick])
1336 ################################################################################
1337 # axis painter
1338 ################################################################################
1341 class axiscanvas(canvas._canvas):
1342 """axis canvas
1343 - an axis canvas is a regular canvas returned by an
1344 axispainters painter method
1345 - it contains a PyX length extent to be used for the
1346 alignment of additional axes; the axis extent should
1347 be handled by the axispainters painter method; you may
1348 apprehend this as a size information comparable to a
1349 bounding box, which must be handled manually
1350 - it contains a list of textboxes called labels which are
1351 used to rate the distances between the labels if needed
1352 by the axis later on; the painter method has not only to
1353 insert the labels into this canvas, but should also fill
1354 this list, when a rating of the distances should be
1355 performed by the axis"""
1357 # __implements__ = sole implementation
1359 def __init__(self, *args, **kwargs):
1360 """initializes the instance
1361 - sets extent to zero
1362 - sets labels to an empty list"""
1363 canvas._canvas.__init__(self, *args, **kwargs)
1364 self.extent = 0
1365 self.labels = []
1368 class rotatetext:
1369 """create rotations accordingly to tick directions
1370 - upsidedown rotations are suppressed by rotating them by another 180 degree"""
1372 # __implements__ = sole implementation
1374 def __init__(self, direction, epsilon=1e-10):
1375 """initializes the instance
1376 - direction is an angle to be used relative to the tick direction
1377 - epsilon is the value by which 90 degrees can be exceeded before
1378 an 180 degree rotation is added"""
1379 self.direction = direction
1380 self.epsilon = epsilon
1382 def trafo(self, dx, dy):
1383 """returns a rotation transformation accordingly to the tick direction
1384 - dx and dy are the direction of the tick"""
1385 direction = self.direction + math.atan2(dy, dx) * 180 / math.pi
1386 while (direction > 180 + self.epsilon):
1387 direction -= 360
1388 while (direction < -180 - self.epsilon):
1389 direction += 360
1390 while (direction > 90 + self.epsilon):
1391 direction -= 180
1392 while (direction < -90 - self.epsilon):
1393 direction += 180
1394 return trafomodule.rotate(direction)
1397 rotatetext.parallel = rotatetext(90)
1398 rotatetext.orthogonal = rotatetext(180)
1401 class _Iaxispainter:
1402 "class for painting axes"
1404 def paint(self, axispos, axis, ac=None):
1405 """paint the axis into an axiscanvas
1406 - returns the axiscanvas
1407 - when no axiscanvas is provided (the typical case), a new
1408 axiscanvas is created. however, when extending an painter
1409 by inheritance, painting on the same axiscanvas is supported
1410 by setting the axiscanvas attribute
1411 - axispos is an instance, which implements _Iaxispos to
1412 define the tick positions
1413 - the axis and should not be modified (we may
1414 add some temporary variables like axis.ticks[i].temp_xxx,
1415 which might be used just temporary) -- the idea is that
1416 all things can be used several times
1417 - also do not modify the instance (self) -- even this
1418 instance might be used several times; thus do not modify
1419 attributes like self.titleattrs etc. (use local copies)
1420 - the method might access some additional attributes from
1421 the axis, e.g. the axis title -- the axis painter should
1422 document this behavior and rely on the availability of
1423 those attributes -> it becomes a question of the proper
1424 usage of the combination of axis & axispainter
1425 - the axiscanvas is a axiscanvas instance and should be
1426 filled with ticks, labels, title, etc.; note that the
1427 extent and labels instance variables should be handled
1428 as documented in the axiscanvas"""
1431 class _Iaxispos:
1432 """interface definition of axis tick position methods
1433 - these methods are used for the postitioning of the ticks
1434 when painting an axis"""
1435 # TODO: should we add a local transformation (for label text etc?)
1436 # (this might replace tickdirection (and even tickposition?))
1438 def basepath(self, x1=None, x2=None):
1439 """return the basepath as a path
1440 - x1 is the start position; if not set, the basepath starts
1441 from the beginning of the axis, which might imply a
1442 value outside of the graph coordinate range [0; 1]
1443 - x2 is analogous to x1, but for the end position"""
1445 def vbasepath(self, v1=None, v2=None):
1446 """return the basepath as a path
1447 - like basepath, but for graph coordinates"""
1449 def gridpath(self, x):
1450 """return the gridpath as a path for a given position x
1451 - might return None when no gridpath is available"""
1453 def vgridpath(self, v):
1454 """return the gridpath as a path for a given position v
1455 in graph coordinates
1456 - might return None when no gridpath is available"""
1458 def tickpoint_pt(self, x):
1459 """return the position at the basepath as a tuple (x, y) in
1460 postscript points for the position x"""
1462 def tickpoint(self, x):
1463 """return the position at the basepath as a tuple (x, y) in
1464 in PyX length for the position x"""
1466 def vtickpoint_pt(self, v):
1467 "like tickpoint_pt, but for graph coordinates"
1469 def vtickpoint(self, v):
1470 "like tickpoint, but for graph coordinates"
1472 def tickdirection(self, x):
1473 """return the direction of a tick as a tuple (dx, dy) for the
1474 position x (the direction points towards the graph)"""
1476 def vtickdirection(self, v):
1477 """like tickposition, but for graph coordinates"""
1480 class _axispos:
1481 """implements those parts of _Iaxispos which can be build
1482 out of the axis convert method and other _Iaxispos methods
1483 - base _Iaxispos methods, which need to be implemented:
1484 - vbasepath
1485 - vgridpath
1486 - vtickpoint_pt
1487 - vtickdirection
1488 - other methods needed for _Iaxispos are build out of those
1489 listed above when this class is inherited"""
1491 def __init__(self, convert):
1492 """initializes the instance
1493 - convert is a convert method from an axis"""
1494 self.convert = convert
1496 def basepath(self, x1=None, x2=None):
1497 if x1 is None:
1498 if x2 is None:
1499 return self.vbasepath()
1500 else:
1501 return self.vbasepath(v2=self.convert(x2))
1502 else:
1503 if x2 is None:
1504 return self.vbasepath(v1=self.convert(x1))
1505 else:
1506 return self.vbasepath(v1=self.convert(x1), v2=self.convert(x2))
1508 def gridpath(self, x):
1509 return self.vgridpath(self.convert(x))
1511 def tickpoint_pt(self, x):
1512 return self.vtickpoint_pt(self.convert(x))
1514 def tickpoint(self, x):
1515 return self.vtickpoint(self.convert(x))
1517 def vtickpoint(self, v):
1518 return [unit.t_pt(x) for x in self.vtickpoint(v)]
1520 def tickdirection(self, x):
1521 return self.vtickdirection(self.convert(x))
1524 class pathaxispos(_axispos):
1525 """axis tick position methods along an arbitrary path"""
1527 __implements__ = _Iaxispos
1529 def __init__(self, p, convert, direction=1):
1530 self.path = p
1531 self.normpath = path.normpath(p)
1532 self.arclength = self.normpath.arclength(p)
1533 _axispos.__init__(self, convert)
1534 self.direction = direction
1536 def vbasepath(self, v1=None, v2=None):
1537 if v1 is None:
1538 if v2 is None:
1539 return self.path
1540 else:
1541 return self.normpath.split(self.normpath.lentopar(v2 * self.arclength))[0]
1542 else:
1543 if v2 is None:
1544 return self.normpath.split(self.normpath.lentopar(v1 * self.arclength))[1]
1545 else:
1546 return self.normpath.split(*self.normpath.lentopar([v1 * self.arclength, v2 * self.arclength]))[1]
1548 def vgridpath(self, v):
1549 return None
1551 def vtickpoint_pt(self, v):
1552 # XXX: path._at missing!
1553 return [unit.topt(x) for x in self.normpath.at(self.normpath.lentopar(v * self.arclength))]
1555 def vtickdirection(self, v):
1556 t = self.normpath.tangent(self.normpath.lentopar(v * self.arclength))
1557 # XXX: path._begin and path._end missing!
1558 tbegin = [unit.topt(x) for x in t.begin()]
1559 tend = [unit.topt(x) for x in t.end()]
1560 dx = tend[0]-tbegin[0]
1561 dy = tend[1]-tbegin[1]
1562 norm = math.sqrt(dx*dx + dy*dy)
1563 if self.direction == 1:
1564 return -dy/norm, dx/norm
1565 elif self.direction == -1:
1566 return dy/norm, -dx/norm
1567 raise RuntimeError("unknown direction")
1570 class axistitlepainter:
1571 """class for painting an axis title
1572 - the axis must have a title attribute when using this painter;
1573 this title might be None"""
1575 __implements__ = _Iaxispainter
1577 def __init__(self, titledist="0.3 cm",
1578 titleattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1579 titledirection=rotatetext.parallel,
1580 titlepos=0.5,
1581 texrunner=textmodule.defaulttexrunner):
1582 """initialized the instance
1583 - titledist is a visual PyX length giving the distance
1584 of the title from the axis extent already there (a title might
1585 be added after labels or other things are plotted already)
1586 - titleattrs is a list of attributes for a texrunners text
1587 method; a single is allowed without being a list; None
1588 turns off the title
1589 - titledirection is an instance of rotatetext or None
1590 - titlepos is the position of the title in graph coordinates
1591 - texrunner is the texrunner to be used to create text
1592 (the texrunner is available for further use in derived
1593 classes as instance variable texrunner)"""
1594 self.titledist_str = titledist
1595 self.titleattrs = titleattrs
1596 self.titledirection = titledirection
1597 self.titlepos = titlepos
1598 self.texrunner = texrunner
1600 def paint(self, axispos, axis, ac=None):
1601 if ac is None:
1602 ac = axiscanvas()
1603 if axis.title is not None and self.titleattrs is not None:
1604 titledist = unit.length(self.titledist_str, default_type="v")
1605 x, y = axispos.vtickpoint_pt(self.titlepos)
1606 dx, dy = axispos.vtickdirection(self.titlepos)
1607 titleattrs = helper.ensurelist(self.titleattrs)
1608 if self.titledirection is not None:
1609 titleattrs.append(self.titledirection.trafo(dx, dy))
1610 title = self.texrunner.text_pt(x, y, axis.title, *titleattrs)
1611 ac.extent += titledist
1612 title.linealign(ac.extent, -dx, -dy)
1613 ac.extent += title.extent(dx, dy)
1614 ac.insert(title)
1615 return ac
1618 class axispainter(axistitlepainter):
1619 """class for painting the ticks and labels of an axis
1620 - the inherited titleaxispainter is used to paint the title of
1621 the axis
1622 - note that the type of the elements of ticks given as an argument
1623 of the paint method must be suitable for the tick position methods
1624 of the axis"""
1626 __implements__ = _Iaxispainter
1628 defaultticklengths = ["%0.5f cm" % (0.2*goldenmean**(-i)) for i in range(10)]
1630 def __init__(self, innerticklengths=defaultticklengths,
1631 outerticklengths=None,
1632 tickattrs=(),
1633 gridattrs=None,
1634 zeropathattrs=(),
1635 basepathattrs=style.linecap.square,
1636 labeldist="0.3 cm",
1637 labelattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1638 labeldirection=None,
1639 labelhequalize=0,
1640 labelvequalize=1,
1641 **kwargs):
1642 """initializes the instance
1643 - innerticklenths and outerticklengths are two lists of
1644 visual PyX lengths for ticks, subticks, etc. plotted inside
1645 and outside of the graph; when a single value is given, it
1646 is used for all tick levels; None turns off ticks inside or
1647 outside of the graph
1648 - tickattrs are a list of stroke attributes for the ticks;
1649 a single entry is allowed without being a list; None turns
1650 off ticks
1651 - gridattrs are a list of lists used as stroke
1652 attributes for ticks, subticks etc.; when a single list
1653 is given, it is used for ticks, subticks, etc.; a single
1654 entry is allowed without being a list; None turns off
1655 the grid
1656 - zeropathattrs are a list of stroke attributes for a grid
1657 line at axis value zero; a single entry is allowed without
1658 being a list; None turns off the zeropath
1659 - basepathattrs are a list of stroke attributes for a grid
1660 line at axis value zero; a single entry is allowed without
1661 being a list; None turns off the basepath
1662 - labeldist is a visual PyX length for the distance of the labels
1663 from the axis basepath
1664 - labelattrs is a list of attributes for a texrunners text
1665 method; a single entry is allowed without being a list;
1666 None turns off the labels
1667 - titledirection is an instance of rotatetext or None
1668 - labelhequalize and labelvequalize (booleans) perform an equal
1669 alignment for straight vertical and horizontal axes, respectively
1670 - futher keyword arguments are passed to axistitlepainter"""
1671 # TODO: access to axis.divisor -- document, remove, ... ???
1672 self.innerticklengths_str = innerticklengths
1673 self.outerticklengths_str = outerticklengths
1674 self.tickattrs = tickattrs
1675 self.gridattrs = gridattrs
1676 self.zeropathattrs = zeropathattrs
1677 self.basepathattrs = basepathattrs
1678 self.labeldist_str = labeldist
1679 self.labelattrs = labelattrs
1680 self.labeldirection = labeldirection
1681 self.labelhequalize = labelhequalize
1682 self.labelvequalize = labelvequalize
1683 axistitlepainter.__init__(self, **kwargs)
1685 def paint(self, axispos, axis, ac=None):
1686 if ac is None:
1687 ac = axiscanvas()
1688 else:
1689 raise RuntimeError("XXX") # XXX debug only
1690 labeldist = unit.length(self.labeldist_str, default_type="v")
1691 for tick in axis.ticks:
1692 tick.temp_v = axis.convert(float(tick) * axis.divisor)
1693 tick.temp_x, tick.temp_y = axispos.vtickpoint_pt(tick.temp_v)
1694 tick.temp_dx, tick.temp_dy = axispos.vtickdirection(tick.temp_v)
1696 # create & align tick.temp_labelbox
1697 for tick in axis.ticks:
1698 if tick.labellevel is not None:
1699 labelattrs = helper.getsequenceno(self.labelattrs, tick.labellevel)
1700 if labelattrs is not None:
1701 labelattrs = helper.ensurelist(labelattrs)[:]
1702 if self.labeldirection is not None:
1703 labelattrs.append(self.labeldirection.trafo(tick.temp_dx, tick.temp_dy))
1704 if tick.labelattrs is not None:
1705 labelattrs.extend(helper.ensurelist(tick.labelattrs))
1706 tick.temp_labelbox = self.texrunner.text_pt(tick.temp_x, tick.temp_y, tick.label, *labelattrs)
1707 if len(axis.ticks) > 1:
1708 equaldirection = 1
1709 for tick in axis.ticks[1:]:
1710 if tick.temp_dx != axis.ticks[0].temp_dx or tick.temp_dy != axis.ticks[0].temp_dy:
1711 equaldirection = 0
1712 else:
1713 equaldirection = 0
1714 if equaldirection and ((not axis.ticks[0].temp_dx and self.labelvequalize) or
1715 (not axis.ticks[0].temp_dy and self.labelhequalize)):
1716 if self.labelattrs is not None:
1717 box.linealignequal([tick.temp_labelbox for tick in axis.ticks if tick.labellevel is not None],
1718 labeldist, -axis.ticks[0].temp_dx, -axis.ticks[0].temp_dy)
1719 else:
1720 for tick in axis.ticks:
1721 if tick.labellevel is not None and self.labelattrs is not None:
1722 tick.temp_labelbox.linealign(labeldist, -tick.temp_dx, -tick.temp_dy)
1724 def mkv(arg):
1725 if helper.issequence(arg):
1726 return [unit.length(a, default_type="v") for a in arg]
1727 if arg is not None:
1728 return unit.length(arg, default_type="v")
1729 innerticklengths = mkv(self.innerticklengths_str)
1730 outerticklengths = mkv(self.outerticklengths_str)
1732 for tick in axis.ticks:
1733 if tick.ticklevel is not None:
1734 innerticklength = helper.getitemno(innerticklengths, tick.ticklevel)
1735 outerticklength = helper.getitemno(outerticklengths, tick.ticklevel)
1736 if innerticklength is not None or outerticklength is not None:
1737 if innerticklength is None:
1738 innerticklength = 0
1739 if outerticklength is None:
1740 outerticklength = 0
1741 tickattrs = helper.getsequenceno(self.tickattrs, tick.ticklevel)
1742 if tickattrs is not None:
1743 innerticklength_pt = unit.topt(innerticklength)
1744 outerticklength_pt = unit.topt(outerticklength)
1745 x1 = tick.temp_x + tick.temp_dx * innerticklength_pt
1746 y1 = tick.temp_y + tick.temp_dy * innerticklength_pt
1747 x2 = tick.temp_x - tick.temp_dx * outerticklength_pt
1748 y2 = tick.temp_y - tick.temp_dy * outerticklength_pt
1749 ac.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(tickattrs))
1750 if tick != frac((0, 1)) or self.zeropathattrs is None:
1751 gridattrs = helper.getsequenceno(self.gridattrs, tick.ticklevel)
1752 if gridattrs is not None:
1753 ac.stroke(axispos.vgridpath(tick.temp_v), *helper.ensuresequence(gridattrs))
1754 if outerticklength is not None and unit.topt(outerticklength) > unit.topt(ac.extent):
1755 ac.extent = outerticklength
1756 if outerticklength is not None and unit.topt(-innerticklength) > unit.topt(ac.extent):
1757 ac.extent = -innerticklength
1758 if tick.labellevel is not None and self.labelattrs is not None:
1759 ac.insert(tick.temp_labelbox)
1760 ac.labels.append(tick.temp_labelbox)
1761 extent = tick.temp_labelbox.extent(tick.temp_dx, tick.temp_dy) + labeldist
1762 if unit.topt(extent) > unit.topt(ac.extent):
1763 ac.extent = extent
1764 if self.basepathattrs is not None:
1765 ac.stroke(axispos.vbasepath(), *helper.ensuresequence(self.basepathattrs))
1766 if self.zeropathattrs is not None:
1767 if len(axis.ticks) and axis.ticks[0] * axis.ticks[-1] < frac((0, 1)):
1768 ac.stroke(axispos.gridpath(0), *helper.ensuresequence(self.zeropathattrs))
1770 # for tick in axis.ticks:
1771 # del tick.temp_v # we've inserted those temporary variables ... and do not care any longer about them
1772 # del tick.temp_x
1773 # del tick.temp_y
1774 # del tick.temp_dx
1775 # del tick.temp_dy
1776 # if tick.labellevel is not None and self.labelattrs is not None:
1777 # del tick.temp_labelbox
1779 axistitlepainter.paint(self, axispos, axis, ac=ac)
1781 return ac
1784 class linkaxispainter(axispainter):
1785 """class for painting a linked axis
1786 - the inherited axispainter is used to paint the axis
1787 - modifies some constructor defaults"""
1789 __implements__ = _Iaxispainter
1791 def __init__(self, zeropathattrs=None,
1792 labelattrs=None,
1793 titleattrs=None,
1794 **kwargs):
1795 """initializes the instance
1796 - the zeropathattrs default is set to None thus skipping the zeropath
1797 - the labelattrs default is set to None thus skipping the labels
1798 - the titleattrs default is set to None thus skipping the title
1799 - all keyword arguments are passed to axispainter"""
1800 axispainter.__init__(self, zeropathattrs=zeropathattrs,
1801 labelattrs=labelattrs,
1802 titleattrs=titleattrs,
1803 **kwargs)
1806 class subaxispos:
1807 """implementation of the _Iaxispos interface for a subaxis"""
1809 __implements__ = _Iaxispos
1811 def __init__(self, convert, baseaxispos, vmin, vmax, vminover, vmaxover):
1812 """initializes the instance
1813 - convert is the subaxis convert method
1814 - baseaxispos is the axispos instance of the base axis
1815 - vmin, vmax is the range covered by the subaxis in graph coordinates
1816 - vminover, vmaxover is the extended range of the subaxis including
1817 regions between several subaxes (for baseline drawing etc.)"""
1818 self.convert = convert
1819 self.baseaxispos = baseaxispos
1820 self.vmin = vmin
1821 self.vmax = vmax
1822 self.vminover = vminover
1823 self.vmaxover = vmaxover
1825 def basepath(self, x1=None, x2=None):
1826 if x1 is not None:
1827 v1 = self.vmin+self.convert(x1)*(self.vmax-self.vmin)
1828 else:
1829 v1 = self.vminover
1830 if x2 is not None:
1831 v2 = self.vmin+self.convert(x2)*(self.vmax-self.vmin)
1832 else:
1833 v2 = self.vmaxover
1834 return self.baseaxispos.vbasepath(v1, v2)
1836 def vbasepath(self, v1=None, v2=None):
1837 if v1 is not None:
1838 v1 = self.vmin+v1*(self.vmax-self.vmin)
1839 else:
1840 v1 = self.vminover
1841 if v2 is not None:
1842 v2 = self.vmin+v2*(self.vmax-self.vmin)
1843 else:
1844 v2 = self.vmaxover
1845 return self.baseaxispos.vbasepath(v1, v2)
1847 def gridpath(self, x):
1848 return self.baseaxispos.vgridpath(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1850 def vgridpath(self, v):
1851 return self.baseaxispos.vgridpath(self.vmin+v*(self.vmax-self.vmin))
1853 def tickpoint_pt(self, x, axis=None):
1854 return self.baseaxispos.vtickpoint_pt(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1856 def tickpoint(self, x, axis=None):
1857 return self.baseaxispos.vtickpoint(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1859 def vtickpoint_pt(self, v, axis=None):
1860 return self.baseaxispos.vtickpoint_pt(self.vmin+v*(self.vmax-self.vmin))
1862 def vtickpoint(self, v, axis=None):
1863 return self.baseaxispos.vtickpoint(self.vmin+v*(self.vmax-self.vmin))
1865 def tickdirection(self, x, axis=None):
1866 return self.baseaxispos.vtickdirection(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1868 def vtickdirection(self, v, axis=None):
1869 return self.baseaxispos.vtickdirection(self.vmin+v*(self.vmax-self.vmin))
1872 class splitaxispainter(axistitlepainter):
1873 """class for painting a splitaxis
1874 - the inherited titleaxispainter is used to paint the title of
1875 the axis
1876 - the splitaxispainter access the subaxes attribute of the axis"""
1878 __implements__ = _Iaxispainter
1880 def __init__(self, breaklinesdist="0.05 cm",
1881 breaklineslength="0.5 cm",
1882 breaklinesangle=-60,
1883 breaklinesattrs=(),
1884 **args):
1885 """initializes the instance
1886 - breaklinesdist is a visual length of the distance between
1887 the two lines of the axis break
1888 - breaklineslength is a visual length of the length of the
1889 two lines of the axis break
1890 - breaklinesangle is the angle of the lines of the axis break
1891 - breaklinesattrs are a list of stroke attributes for the
1892 axis break lines; a single entry is allowed without being a
1893 list; None turns off the break lines
1894 - futher keyword arguments are passed to axistitlepainter"""
1895 self.breaklinesdist_str = breaklinesdist
1896 self.breaklineslength_str = breaklineslength
1897 self.breaklinesangle = breaklinesangle
1898 self.breaklinesattrs = breaklinesattrs
1899 axistitlepainter.__init__(self, **args)
1901 def paint(self, axispos, axis, ac=None):
1902 if ac is None:
1903 ac = axiscanvas()
1904 else:
1905 raise RuntimeError("XXX") # XXX debug only
1906 for subaxis in axis.subaxes:
1907 subaxis.finish(subaxispos(subaxis.convert, axispos, subaxis.vmin, subaxis.vmax, subaxis.vminover, subaxis.vmaxover))
1908 ac.insert(subaxis.axiscanvas)
1909 if unit.topt(ac.extent) < unit.topt(subaxis.axiscanvas.extent):
1910 ac.extent = subaxis.axiscanvas.extent
1911 if self.breaklinesattrs is not None:
1912 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1913 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1914 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1915 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1916 breaklinesextent = (0.5*self.breaklinesdist*math.fabs(self.cos) +
1917 0.5*self.breaklineslength*math.fabs(self.sin))
1918 if unit.topt(ac.extent) < unit.topt(breaklinesextent):
1919 ac.extent = breaklinesextent
1920 for subaxis1, subaxis2 in zip(axis.subaxes[:-1], axis.subaxes[1:]):
1921 # use a tangent of the basepath (this is independent of the tickdirection)
1922 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1923 p = path.normpath(axispos.vbasepath(v, None))
1924 breakline = p.tangent(0, self.breaklineslength)
1925 widthline = p.tangent(0, self.breaklinesdist).transformed(trafomodule.rotate(self.breaklinesangle+90, *breakline.begin()))
1926 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1927 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1928 breakline = breakline.transformed(trafomodule.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1929 breakline1 = breakline.transformed(trafomodule.translate(*towidth))
1930 breakline2 = breakline.transformed(trafomodule.translate(-towidth[0], -towidth[1]))
1931 ac.fill(path.path(path.moveto(*breakline1.begin()),
1932 path.lineto(*breakline1.end()),
1933 path.lineto(*breakline2.end()),
1934 path.lineto(*breakline2.begin()),
1935 path.closepath()), color.gray.white)
1936 ac.stroke(breakline1, *helper.ensuresequence(self.breaklinesattrs))
1937 ac.stroke(breakline2, *helper.ensuresequence(self.breaklinesattrs))
1938 axistitlepainter.paint(self, axispos, axis, ac=ac)
1939 return ac
1942 class linksplitaxispainter(splitaxispainter):
1943 """class for painting a linked splitaxis
1944 - the inherited splitaxispainter is used to paint the axis
1945 - modifies some constructor defaults"""
1947 __implements__ = _Iaxispainter
1949 def __init__(self, titleattrs=None, **kwargs):
1950 """initializes the instance
1951 - the titleattrs default is set to None thus skipping the title
1952 - all keyword arguments are passed to splitaxispainter"""
1953 splitaxispainter.__init__(self, titleattrs=titleattrs, **kwargs)
1956 class baraxispainter(axistitlepainter):
1957 """class for painting a baraxis
1958 - the inherited titleaxispainter is used to paint the title of
1959 the axis
1960 - the baraxispainter access the multisubaxis, subaxis names, texts, and
1961 relsizes attributes"""
1963 __implements__ = _Iaxispainter
1965 def __init__(self, innerticklength=None,
1966 outerticklength=None,
1967 tickattrs=(),
1968 basepathattrs=style.linecap.square,
1969 namedist="0.3 cm",
1970 nameattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1971 namedirection=None,
1972 namepos=0.5,
1973 namehequalize=0,
1974 namevequalize=1,
1975 **args):
1976 """initializes the instance
1977 - innerticklength and outerticklength are a visual length of
1978 the ticks to be plotted at the axis basepath to visually
1979 separate the bars; if neither innerticklength nor
1980 outerticklength are set, not ticks are plotted
1981 - breaklinesattrs are a list of stroke attributes for the
1982 axis tick; a single entry is allowed without being a
1983 list; None turns off the ticks
1984 - namedist is a visual PyX length for the distance of the bar
1985 names from the axis basepath
1986 - nameattrs is a list of attributes for a texrunners text
1987 method; a single entry is allowed without being a list;
1988 None turns off the names
1989 - namedirection is an instance of rotatetext or None
1990 - namehequalize and namevequalize (booleans) perform an equal
1991 alignment for straight vertical and horizontal axes, respectively
1992 - futher keyword arguments are passed to axistitlepainter"""
1993 self.innerticklength_str = innerticklength
1994 self.outerticklength_str = outerticklength
1995 self.tickattrs = tickattrs
1996 self.basepathattrs = basepathattrs
1997 self.namedist_str = namedist
1998 self.nameattrs = nameattrs
1999 self.namedirection = namedirection
2000 self.namepos = namepos
2001 self.namehequalize = namehequalize
2002 self.namevequalize = namevequalize
2003 axistitlepainter.__init__(self, **args)
2005 def paint(self, axispos, axis, ac=None):
2006 if ac is None:
2007 ac = axiscanvas()
2008 else:
2009 raise RuntimeError("XXX") # XXX debug only
2010 if axis.multisubaxis is not None:
2011 for subaxis in axis.subaxis:
2012 subaxis.finish(subaxispos(subaxis.convert, axispos, subaxis.vmin, subaxis.vmax, None, None))
2013 ac.insert(subaxis.axiscanvas)
2014 if unit.topt(ac.extent) < unit.topt(subaxis.axiscanvas.extent):
2015 ac.extent = subaxis.axiscanvas.extent
2016 namepos = []
2017 for name in axis.names:
2018 v = axis.convert((name, self.namepos))
2019 x, y = axispos.vtickpoint_pt(v)
2020 dx, dy = axispos.vtickdirection(v)
2021 namepos.append((v, x, y, dx, dy))
2022 nameboxes = []
2023 if self.nameattrs is not None:
2024 for (v, x, y, dx, dy), name in zip(namepos, axis.names):
2025 nameattrs = helper.ensurelist(self.nameattrs)[:]
2026 if self.namedirection is not None:
2027 nameattrs.append(self.namedirection.trafo(tick.temp_dx, tick.temp_dy))
2028 if axis.texts.has_key(name):
2029 nameboxes.append(self.texrunner.text_pt(x, y, str(axis.texts[name]), *nameattrs))
2030 elif axis.texts.has_key(str(name)):
2031 nameboxes.append(self.texrunner.text_pt(x, y, str(axis.texts[str(name)]), *nameattrs))
2032 else:
2033 nameboxes.append(self.texrunner.text_pt(x, y, str(name), *nameattrs))
2034 labeldist = ac.extent + unit.length(self.namedist_str, default_type="v")
2035 if len(namepos) > 1:
2036 equaldirection = 1
2037 for np in namepos[1:]:
2038 if np[3] != namepos[0][3] or np[4] != namepos[0][4]:
2039 equaldirection = 0
2040 else:
2041 equaldirection = 0
2042 if equaldirection and ((not namepos[0][3] and self.namevequalize) or
2043 (not namepos[0][4] and self.namehequalize)):
2044 box.linealignequal(nameboxes, labeldist, -namepos[0][3], -namepos[0][4])
2045 else:
2046 for namebox, np in zip(nameboxes, namepos):
2047 namebox.linealign(labeldist, -np[3], -np[4])
2048 if self.innerticklength_str is not None:
2049 innerticklength = unit.length(self.innerticklength_str, default_type="v")
2050 innerticklength_pt = unit.topt(innerticklength)
2051 if self.tickattrs is not None and unit.topt(ac.extent) < -innerticklength_pt:
2052 ac.extent = -innerticklength
2053 elif self.outerticklength_str is not None:
2054 innerticklength = innerticklength_pt = 0
2055 if self.outerticklength_str is not None:
2056 outerticklength = unit.length(self.outerticklength_str, default_type="v")
2057 outerticklength_pt = unit.topt(outerticklength)
2058 if self.tickattrs is not None and unit.topt(ac.extent) < outerticklength_pt:
2059 ac.extent = outerticklength
2060 elif self.innerticklength_str is not None:
2061 outerticklength = outerticklength_pt = 0
2062 for (v, x, y, dx, dy), namebox in zip(namepos, nameboxes):
2063 newextent = namebox.extent(dx, dy) + labeldist
2064 if unit.topt(ac.extent) < unit.topt(newextent):
2065 ac.extent = newextent
2066 if self.tickattrs is not None and (self.innerticklength_str is not None or self.outerticklength_str is not None):
2067 for pos in axis.relsizes:
2068 if pos == axis.relsizes[0]:
2069 pos -= axis.firstdist
2070 elif pos != axis.relsizes[-1]:
2071 pos -= 0.5 * axis.dist
2072 v = pos / axis.relsizes[-1]
2073 x, y = axispos.vtickpoint_pt(v)
2074 dx, dy = axispos.vtickdirection(v)
2075 x1 = x + dx * innerticklength_pt
2076 y1 = y + dy * innerticklength_pt
2077 x2 = x - dx * outerticklength_pt
2078 y2 = y - dy * outerticklength_pt
2079 ac.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(self.tickattrs))
2080 if self.basepathattrs is not None:
2081 p = axispos.vbasepath()
2082 if p is not None:
2083 ac.stroke(p, *helper.ensuresequence(self.basepathattrs))
2084 for namebox in nameboxes:
2085 ac.insert(namebox)
2086 axistitlepainter.paint(self, axispos, axis, ac=ac)
2087 return ac
2090 class linkbaraxispainter(baraxispainter):
2091 """class for painting a linked baraxis
2092 - the inherited baraxispainter is used to paint the axis
2093 - modifies some constructor defaults"""
2095 __implements__ = _Iaxispainter
2097 def __init__(self, nameattrs=None, titleattrs=None, **kwargs):
2098 """initializes the instance
2099 - the titleattrs default is set to None thus skipping the title
2100 - the nameattrs default is set to None thus skipping the names
2101 - all keyword arguments are passed to axispainter"""
2102 baraxispainter.__init__(self, nameattrs=nameattrs, titleattrs=titleattrs, **kwargs)
2105 ################################################################################
2106 # axes
2107 ################################################################################
2110 class _Iaxis:
2111 """interface definition of a axis
2112 - an axis should implement an convert and invert method like
2113 _Imap, but this is not part of this interface definition;
2114 one possibility is to mix-in a proper map class, but special
2115 purpose axes might do something else
2116 - an axis has the instance variable axiscanvas after the finish
2117 method was called
2118 - an axis might have further instance variables (title, ticks)
2119 to be used in combination with appropriate axispainters"""
2121 def convert(self, x):
2122 "convert a value into graph coordinates"
2124 def invert(self, v):
2125 "invert a graph coordinate to a axis value"
2127 def getrelsize(self):
2128 """returns the relative size (width) of the axis
2129 - for use in splitaxis, baraxis etc.
2130 - might return None if no size is available"""
2132 def setrange(self, min=None, max=None):
2133 """set the axis data range
2134 - the type of min and max must fit to the axis
2135 - min<max; the axis might be reversed, but this is
2136 expressed internally only (min<max all the time)
2137 - the axis might not apply the change of the range
2138 (e.g. when the axis range is fixed by the user),
2139 but usually the range is extended to contain the
2140 given range
2141 - for invalid parameters (e.g. negativ values at an
2142 logarithmic axis), an exception should be raised
2143 - a RuntimeError is raised, when setrange is called
2144 after the finish method"""
2146 def getrange(self):
2147 """return data range as a tuple (min, max)
2148 - min<max; the axis might be reversed, but this is
2149 expressed internally only
2150 - a RuntimeError exception is raised when no
2151 range is available"""
2153 def finish(self, axispos):
2154 """finishes the axis
2155 - axispos implements _Iaxispos
2156 - sets the instance axiscanvas, which is insertable into the
2157 graph to finally paint the axis
2158 - any modification of the axis range should be disabled after
2159 the finish method was called"""
2160 # TODO: be more specific about exceptions
2162 def createlinkaxis(self, **kwargs):
2163 """create a link axis to the axis itself
2164 - typically, a link axis is a axis, which share almost
2165 all properties with the axis it is linked to
2166 - typically, the painter gets replaced by a painter
2167 which doesn't put any text to the axis"""
2170 class _axis:
2171 """base implementation a regular axis
2172 - typical usage is to mix-in a linmap or a logmap to
2173 complete the axis interface
2174 - note that some methods of this class want to access a
2175 parter and a rater; those attributes implementing _Iparter
2176 and _Irater should be initialized by the constructors
2177 of derived classes"""
2179 def __init__(self, min=None, max=None, reverse=0, divisor=1,
2180 title=None, painter=axispainter(), texter=defaulttexter(),
2181 density=1, maxworse=2, manualticks=[]):
2182 """initializes the instance
2183 - min and max fix the axis minimum and maximum, respectively;
2184 they are determined by the data to be plotted, when not fixed
2185 - reverse (boolean) reverses the minimum and the maximum of
2186 the axis
2187 - numerical divisor for the axis partitioning
2188 - title is a string containing the axis title
2189 - axispainter is the axis painter (should implement _Ipainter)
2190 - texter is the texter (should implement _Itexter)
2191 - density is a global parameter for the axis paritioning and
2192 axis rating; its default is 1, but the range 0.5 to 2.5 should
2193 be usefull to get less or more ticks by the automatic axis
2194 partitioning
2195 - maxworse is a number of trials with worse tick rating
2196 before giving up (usually it should not be needed to increase
2197 this value; increasing the number will slow down the automatic
2198 axis partitioning considerably)
2199 - manualticks and the partitioner results are mixed
2200 by _mergeticklists
2201 - note that some methods of this class want to access a
2202 parter and a rater; those attributes implementing _Iparter
2203 and _Irater should be initialized by the constructors
2204 of derived classes"""
2205 if min is not None and max is not None and min > max:
2206 min, max, reverse = max, min, not reverse
2207 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
2208 self.divisor = divisor
2209 self.title = title
2210 self.painter = painter
2211 self.texter = texter
2212 self.density = density
2213 self.maxworse = maxworse
2214 self.manualticks = self.checkfraclist(manualticks)
2215 self.canconvert = 0
2216 self.axiscanvas = None
2217 self._setrange()
2219 def _setrange(self, min=None, max=None):
2220 if not self.fixmin and min is not None and (self.min is None or min < self.min):
2221 self.min = min
2222 if not self.fixmax and max is not None and (self.max is None or max > self.max):
2223 self.max = max
2224 if None not in (self.min, self.max):
2225 self.canconvert = 1
2226 if self.reverse:
2227 self.setbasepoints(((self.min, 1), (self.max, 0)))
2228 else:
2229 self.setbasepoints(((self.min, 0), (self.max, 1)))
2231 def _getrange(self):
2232 return self.min, self.max
2234 def _forcerange(self, range):
2235 self.min, self.max = range
2236 self._setrange()
2238 def setrange(self, min=None, max=None):
2239 if self.axiscanvas is not None:
2240 raise RuntimeError("axis was already finished")
2241 self._setrange(min, max)
2243 def getrange(self):
2244 if self.min is not None and self.max is not None:
2245 return self.min, self.max
2247 def checkfraclist(self, fracs):
2248 "orders a list of fracs, equal entries are not allowed"
2249 if not len(fracs): return []
2250 sorted = list(fracs)
2251 sorted.sort()
2252 last = sorted[0]
2253 for item in sorted[1:]:
2254 if last == item:
2255 raise ValueError("duplicate entry found")
2256 last = item
2257 return sorted
2259 def finish(self, axispos):
2260 if self.axiscanvas is not None: return
2262 # lesspart and morepart can be called after defaultpart;
2263 # this works although some axes may share their autoparting,
2264 # because the axes are processed sequentially
2265 first = 1
2266 if self.parter is not None:
2267 min, max = self.getrange()
2268 self.ticks = _mergeticklists(self.manualticks,
2269 self.parter.defaultpart(min/self.divisor,
2270 max/self.divisor,
2271 not self.fixmin,
2272 not self.fixmax))
2273 worse = 0
2274 nextpart = self.parter.lesspart
2275 while nextpart is not None:
2276 newticks = nextpart()
2277 if newticks is not None:
2278 newticks = _mergeticklists(self.manualticks, newticks)
2279 if first:
2280 bestrate = self.rater.rateticks(self, self.ticks, self.density)
2281 bestrate += self.rater.raterange(self.convert(float(self.ticks[-1])/self.divisor)-
2282 self.convert(float(self.ticks[0])/self.divisor), 1)
2283 variants = [[bestrate, self.ticks]]
2284 first = 0
2285 newrate = self.rater.rateticks(self, newticks, self.density)
2286 newrate += self.rater.raterange(self.convert(float(newticks[-1])/self.divisor)-
2287 self.convert(float(newticks[0])/self.divisor), 1)
2288 variants.append([newrate, newticks])
2289 if newrate < bestrate:
2290 bestrate = newrate
2291 worse = 0
2292 else:
2293 worse += 1
2294 else:
2295 worse += 1
2296 if worse == self.maxworse and nextpart == self.parter.lesspart:
2297 worse = 0
2298 nextpart = self.parter.morepart
2299 if worse == self.maxworse and nextpart == self.parter.morepart:
2300 nextpart = None
2301 else:
2302 self.ticks =self.manualticks
2304 # rating, when several choises are available
2305 if not first:
2306 variants.sort()
2307 if self.painter is not None:
2308 i = 0
2309 bestrate = None
2310 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
2311 saverange = self._getrange()
2312 self.ticks = variants[i][1]
2313 if len(self.ticks):
2314 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2315 self.texter.labels(self.ticks)
2316 ac = self.painter.paint(axispos, self)
2317 ratelayout = self.rater.ratelayout(ac, self.density)
2318 if ratelayout is not None:
2319 variants[i][0] += ratelayout
2320 variants[i].append(ac)
2321 else:
2322 variants[i][0] = None
2323 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
2324 bestrate = variants[i][0]
2325 self._forcerange(saverange)
2326 i += 1
2327 if bestrate is None:
2328 raise RuntimeError("no valid axis partitioning found")
2329 variants = [variant for variant in variants[:i] if variant[0] is not None]
2330 variants.sort()
2331 self.ticks = variants[0][1]
2332 if len(self.ticks):
2333 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2334 self.axiscanvas = variants[0][2]
2335 else:
2336 self.ticks = variants[0][1]
2337 self.texter.labels(self.ticks)
2338 if len(self.ticks):
2339 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2340 self.axiscanvas = axiscanvas()
2341 else:
2342 if len(self.ticks):
2343 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2344 self.texter.labels(self.ticks)
2345 if self.painter is not None:
2346 self.axiscanvas = self.painter.paint(axispos, self)
2347 else:
2348 self.axiscanvas = axiscanvas()
2350 def createlinkaxis(self, **args):
2351 return linkaxis(self, **args)
2354 class linaxis(_axis, _linmap):
2355 """implementation of a linear axis"""
2357 __implements__ = _Iaxis
2359 def __init__(self, parter=autolinparter(), rater=axisrater(), **args):
2360 """initializes the instance
2361 - the parter attribute implements _Iparter
2362 - manualticks and the partitioner results are mixed
2363 by _mergeticklists
2364 - the rater implements _Irater and is used to rate different
2365 tick lists created by the partitioner (after merging with
2366 manully set ticks)
2367 - futher keyword arguments are passed to _axis"""
2368 _axis.__init__(self, **args)
2369 if self.fixmin and self.fixmax:
2370 self.relsize = self.max - self.min
2371 self.parter = parter
2372 self.rater = rater
2375 class logaxis(_axis, _logmap):
2376 """implementation of a logarithmic axis"""
2378 __implements__ = _Iaxis
2380 def __init__(self, parter=autologparter(), rater=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
2381 """initializes the instance
2382 - the parter attribute implements _Iparter
2383 - manualticks and the partitioner results are mixed
2384 by _mergeticklists
2385 - the rater implements _Irater and is used to rate different
2386 tick lists created by the partitioner (after merging with
2387 manully set ticks)
2388 - futher keyword arguments are passed to _axis"""
2389 _axis.__init__(self, **args)
2390 if self.fixmin and self.fixmax:
2391 self.relsize = math.log(self.max) - math.log(self.min)
2392 self.parter = parter
2393 self.rater = rater
2396 class linkaxis:
2397 """a axis linked to an already existing regular axis
2398 - almost all properties of the axis are "copied" from the
2399 axis this axis is linked to
2400 - usually, linked axis are used to create an axis to an
2401 existing axis with different painting properties; linked
2402 axis can be used to plot an axis twice at the opposite
2403 sides of a graphxy or even to share an axis between
2404 different graphs!"""
2406 __implements__ = _Iaxis
2408 def __init__(self, linkedaxis, painter=linkaxispainter()):
2409 """initializes the instance
2410 - it gets a axis this linkaxis is linked to
2411 - it gets a painter to be used for this linked axis"""
2412 self.linkedaxis = linkedaxis
2413 self.painter = painter
2414 self.axiscanvas = None
2416 def __getattr__(self, attr):
2417 """access to unkown attributes are handed over to the
2418 axis this linkaxis is linked to"""
2419 return getattr(self.linkedaxis, attr)
2421 def finish(self, axispos):
2422 """finishes the axis
2423 - instead of performing the hole finish process
2424 (paritioning, rating, etc.) just a painter call
2425 is performed"""
2426 if self.axiscanvas is None:
2427 self.linkedaxis.finish(axispos)
2428 self.axiscanvas = self.painter.paint(axispos, self)
2431 class splitaxis:
2432 """implementation of a split axis
2433 - a split axis contains several (sub-)axes with
2434 non-overlapping data ranges -- between these subaxes
2435 the axis is "splitted"
2436 - (just to get sure: a splitaxis can contain other
2437 splitaxes as its subaxes)
2438 - a splitaxis implements the _Iaxispos for its subaxes
2439 by inheritance from _subaxispos"""
2441 __implements__ = _Iaxis, _Iaxispos
2443 def __init__(self, subaxes, splitlist=0.5, splitdist=0.1, relsizesplitdist=1,
2444 title=None, painter=splitaxispainter()):
2445 """initializes the instance
2446 - subaxes is a list of subaxes
2447 - splitlist is a list of graph coordinates, where the splitting
2448 of the main axis should be performed; a single entry (splitting
2449 two axes) doesn't need to be wrapped into a list; if the list
2450 isn't long enough for the subaxes, missing entries are considered
2451 to be None;
2452 - splitdist is the size of the splitting in graph coordinates, when
2453 the associated splitlist entry is not None
2454 - relsizesplitdist: a None entry in splitlist means, that the
2455 position of the splitting should be calculated out of the
2456 relsize values of conrtibuting subaxes (the size of the
2457 splitting is relsizesplitdist in values of the relsize values
2458 of the axes)
2459 - title is the title of the axis as a string
2460 - painter is the painter of the axis; it should be specialized to
2461 the splitaxis
2462 - the relsize of the splitaxis is the sum of the relsizes of the
2463 subaxes including the relsizesplitdist"""
2464 self.subaxes = subaxes
2465 self.painter = painter
2466 self.title = title
2467 self.splitlist = helper.ensurelist(splitlist)
2468 for subaxis in self.subaxes:
2469 subaxis.vmin = None
2470 subaxis.vmax = None
2471 self.subaxes[0].vmin = 0
2472 self.subaxes[0].vminover = None
2473 self.subaxes[-1].vmax = 1
2474 self.subaxes[-1].vmaxover = None
2475 for i in xrange(len(self.splitlist)):
2476 if self.splitlist[i] is not None:
2477 self.subaxes[i].vmax = self.splitlist[i] - 0.5*splitdist
2478 self.subaxes[i].vmaxover = self.splitlist[i]
2479 self.subaxes[i+1].vmin = self.splitlist[i] + 0.5*splitdist
2480 self.subaxes[i+1].vminover = self.splitlist[i]
2481 i = 0
2482 while i < len(self.subaxes):
2483 if self.subaxes[i].vmax is None:
2484 j = relsize = relsize2 = 0
2485 while self.subaxes[i + j].vmax is None:
2486 relsize += self.subaxes[i + j].relsize + relsizesplitdist
2487 j += 1
2488 relsize += self.subaxes[i + j].relsize
2489 vleft = self.subaxes[i].vmin
2490 vright = self.subaxes[i + j].vmax
2491 for k in range(i, i + j):
2492 relsize2 += self.subaxes[k].relsize
2493 self.subaxes[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
2494 relsize2 += 0.5 * relsizesplitdist
2495 self.subaxes[k].vmaxover = self.subaxes[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
2496 relsize2 += 0.5 * relsizesplitdist
2497 self.subaxes[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
2498 if i == 0 and i + j + 1 == len(self.subaxes):
2499 self.relsize = relsize
2500 i += j + 1
2501 else:
2502 i += 1
2504 self.fixmin = self.subaxes[0].fixmin
2505 if self.fixmin:
2506 self.min = self.subaxes[0].min
2507 self.fixmax = self.subaxes[-1].fixmax
2508 if self.fixmax:
2509 self.max = self.subaxes[-1].max
2511 self.axiscanvas = None
2513 def getrange(self):
2514 min = self.subaxes[0].getrange()
2515 max = self.subaxes[-1].getrange()
2516 try:
2517 return min[0], max[1]
2518 except TypeError:
2519 return None
2521 def setrange(self, min, max):
2522 self.subaxes[0].setrange(min, None)
2523 self.subaxes[-1].setrange(None, max)
2525 def convert(self, value):
2526 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2527 if value < self.subaxes[0].max:
2528 return self.subaxes[0].vmin + self.subaxes[0].convert(value)*(self.subaxes[0].vmax-self.subaxes[0].vmin)
2529 for axis in self.subaxes[1:-1]:
2530 if value > axis.min and value < axis.max:
2531 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
2532 if value > self.subaxes[-1].min:
2533 return self.subaxes[-1].vmin + self.subaxes[-1].convert(value)*(self.subaxes[-1].vmax-self.subaxes[-1].vmin)
2534 raise ValueError("value couldn't be assigned to a split region")
2536 def finish(self, axispos):
2537 if self.axiscanvas is None:
2538 self.axiscanvas = self.painter.paint(axispos, self)
2540 def createlinkaxis(self, **args):
2541 return linksplitaxis(self, **args)
2544 class linksplitaxis(linkaxis):
2545 """a splitaxis linked to an already existing splitaxis
2546 - inherits the access to a linked axis -- as before,
2547 basically only the painter is replaced
2548 - it takes care of the creation of linked axes of
2549 the subaxes"""
2551 __implements__ = _Iaxis
2553 def __init__(self, linkedaxis, painter=linksplitaxispainter(), subaxispainter=None):
2554 """initializes the instance
2555 - it gets a axis this linkaxis is linked to
2556 - it gets a painter to be used for this linked axis
2557 - it gets a list of painters to be used for the linkaxes
2558 of the subaxes; if None, the createlinkaxis of the subaxes
2559 are called without a painter parameter; if it is not a
2560 list, the subaxispainter is passed as the painter
2561 parameter to all createlinkaxis of the subaxes"""
2562 linkaxis.__init__(self, linkedaxis, painter=painter)
2563 if subaxispainter is not None:
2564 if helper.issequence(subaxispainter):
2565 if len(linkedaxis.subaxes) != len(subaxispainter):
2566 raise RuntimeError("subaxes and subaxispainter lengths do not fit")
2567 self.subaxes = [a.createlinkaxis(painter=p) for a, p in zip(linkedaxis.subaxes, subaxispainter)]
2568 else:
2569 self.subaxes = [a.createlinkaxis(painter=subaxispainter) for a in linkedaxis.subaxes]
2570 else:
2571 self.subaxes = [a.createlinkaxis() for a in linkedaxis.subaxes]
2574 class baraxis:
2575 """implementation of a axis for bar graphs
2576 - a bar axes is different from a splitaxis by the way it
2577 selects its subaxes: the convert method gets a list,
2578 where the first entry is a name selecting a subaxis out
2579 of a list; instead of the term "bar" or "subaxis" the term
2580 "item" will be used here
2581 - the baraxis stores a list of names be identify the items;
2582 the names might be of any time (strings, integers, etc.);
2583 the names can be printed as the titles for the items, but
2584 alternatively the names might be transformed by the texts
2585 dictionary, which maps a name to a text to be used to label
2586 the items in the painter
2587 - usually, there is only one subaxis, which is used as
2588 the subaxis for all items
2589 - alternatively it is also possible to use another baraxis
2590 as a multisubaxis; it is copied via the createsubaxis
2591 method whenever another subaxis is needed (by that a
2592 nested bar axis with a different number of subbars at
2593 each item can be created)
2594 - any axis can be a subaxis of a baraxis; if no subaxis
2595 is specified at all, the baraxis simulates a linear
2596 subaxis with a fixed range of 0 to 1
2597 - a splitaxis implements the _Iaxispos for its subaxes
2598 by inheritance from _subaxispos when the multisubaxis
2599 feature is turned on"""
2601 def __init__(self, subaxis=None, multisubaxis=None, title=None,
2602 dist=0.5, firstdist=None, lastdist=None, names=None,
2603 texts={}, painter=baraxispainter()):
2604 """initialize the instance
2605 - subaxis contains a axis to be used as the subaxis
2606 for all items
2607 - multisubaxis might contain another baraxis instance
2608 to be used to construct a new subaxis for each item;
2609 (by that a nested bar axis with a different number
2610 of subbars at each item can be created)
2611 - only one of subaxis or multisubaxis can be set; if neither
2612 of them is set, the baraxis behaves like having a linaxis
2613 as its subaxis with a fixed range 0 to 1
2614 - the title attribute contains the axis title as a string
2615 - the dist is a relsize to be used as the distance between
2616 the items
2617 - the firstdist and lastdist are the distance before the
2618 first and after the last item, respectively; when set
2619 to None (the default), 0.5*dist is used
2620 - names is a predefined list of names to identify the
2621 items; if set, the name list is fixed
2622 - texts is a dictionary transforming a name to a text in
2623 the painter; if a name isn't found in the dictionary
2624 it gets used itself
2625 - the relsize of the baraxis is the sum of the
2626 relsizes including all distances between the items"""
2627 self.dist = dist
2628 if firstdist is not None:
2629 self.firstdist = firstdist
2630 else:
2631 self.firstdist = 0.5 * dist
2632 if lastdist is not None:
2633 self.lastdist = lastdist
2634 else:
2635 self.lastdist = 0.5 * dist
2636 self.relsizes = None
2637 self.fixnames = 0
2638 self.names = []
2639 for name in helper.ensuresequence(names):
2640 self.setname(name)
2641 self.fixnames = names is not None
2642 self.multisubaxis = multisubaxis
2643 if self.multisubaxis is not None:
2644 if subaxis is not None:
2645 raise RuntimeError("either use subaxis or multisubaxis")
2646 self.subaxis = [self.createsubaxis() for name in self.names]
2647 else:
2648 self.subaxis = subaxis
2649 self.title = title
2650 self.fixnames = 0
2651 self.texts = texts
2652 self.painter = painter
2653 self.axiscanvas = None
2655 def createsubaxis(self):
2656 return baraxis(subaxis=self.multisubaxis.subaxis,
2657 multisubaxis=self.multisubaxis.multisubaxis,
2658 title=self.multisubaxis.title,
2659 dist=self.multisubaxis.dist,
2660 firstdist=self.multisubaxis.firstdist,
2661 lastdist=self.multisubaxis.lastdist,
2662 names=self.multisubaxis.names,
2663 texts=self.multisubaxis.texts,
2664 painter=self.multisubaxis.painter)
2666 def getrange(self):
2667 # TODO: we do not yet have a proper range handling for a baraxis
2668 return None
2670 def setrange(self, min=None, max=None):
2671 # TODO: we do not yet have a proper range handling for a baraxis
2672 raise RuntimeError("range handling for a baraxis is not implemented")
2674 def setname(self, name, *subnames):
2675 """add a name to identify an item at the baraxis
2676 - by using subnames, nested name definitions are
2677 possible
2678 - a style (or the user itself) might use this to
2679 insert new items into a baraxis
2680 - setting self.relsizes to None forces later recalculation"""
2681 if not self.fixnames:
2682 if name not in self.names:
2683 self.relsizes = None
2684 self.names.append(name)
2685 if self.multisubaxis is not None:
2686 self.subaxis.append(self.createsubaxis())
2687 if (not self.fixnames or name in self.names) and len(subnames):
2688 if self.multisubaxis is not None:
2689 if self.subaxis[self.names.index(name)].setname(*subnames):
2690 self.relsizes = None
2691 else:
2692 if self.subaxis.setname(*subnames):
2693 self.relsizes = None
2694 return self.relsizes is not None
2696 def updaterelsizes(self):
2697 # guess what it does: it recalculates relsize attribute
2698 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
2699 self.relsizes[-1] += self.lastdist - self.dist
2700 if self.multisubaxis is not None:
2701 subrelsize = 0
2702 for i in range(1, len(self.relsizes)):
2703 self.subaxis[i-1].updaterelsizes()
2704 subrelsize += self.subaxis[i-1].relsizes[-1]
2705 self.relsizes[i] += subrelsize
2706 else:
2707 if self.subaxis is None:
2708 subrelsize = 1
2709 else:
2710 self.subaxis.updaterelsizes()
2711 subrelsize = self.subaxis.relsizes[-1]
2712 for i in range(1, len(self.relsizes)):
2713 self.relsizes[i] += i * subrelsize
2715 def convert(self, value):
2716 """baraxis convert method
2717 - the value should be a list, where the first entry is
2718 a member of the names (set in the constructor or by the
2719 setname method); this first entry identifies an item in
2720 the baraxis
2721 - following values are passed to the appropriate subaxis
2722 convert method
2723 - when there is no subaxis, the convert method will behave
2724 like having a linaxis from 0 to 1 as subaxis"""
2725 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2726 if not self.relsizes:
2727 self.updaterelsizes()
2728 pos = self.names.index(value[0])
2729 if len(value) == 2:
2730 if self.subaxis is None:
2731 subvalue = value[1]
2732 else:
2733 if self.multisubaxis is not None:
2734 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
2735 else:
2736 subvalue = value[1] * self.subaxis.relsizes[-1]
2737 else:
2738 if self.multisubaxis is not None:
2739 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
2740 else:
2741 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
2742 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
2744 def finish(self, axispos):
2745 if self.axiscanvas is None:
2746 if self.multisubaxis is not None:
2747 for name, subaxis in zip(self.names, self.subaxis):
2748 subaxis.vmin = self.convert((name, 0))
2749 subaxis.vmax = self.convert((name, 1))
2750 self.axiscanvas = self.painter.paint(axispos, self)
2752 def createlinkaxis(self, **args):
2753 return linkbaraxis(self, **args)
2756 class linkbaraxis(linkaxis):
2757 """a baraxis linked to an already existing baraxis
2758 - inherits the access to a linked axis -- as before,
2759 basically only the painter is replaced
2760 - it must take care of the creation of linked axes of
2761 the subaxes"""
2763 __implements__ = _Iaxis
2765 def __init__(self, linkedaxis, painter=linkbaraxispainter()):
2766 """initializes the instance
2767 - it gets a axis this linkaxis is linked to
2768 - it gets a painter to be used for this linked axis"""
2769 linkaxis.__init__(self, linkedaxis, painter=painter)
2770 if self.multisubaxis is not None:
2771 self.subaxis = [subaxis.createlinkaxis() for subaxis in self.linkedaxis.subaxis]
2772 elif self.subaxis is not None:
2773 self.subaxis = self.subaxis.createlinkaxis()
2776 def pathaxis(path, axis, **kwargs):
2777 """creates an axiscanvas for an axis along a path"""
2778 mypathaxispos = pathaxispos(path, axis.convert, **kwargs)
2779 axis.finish(mypathaxispos)
2780 return axis.axiscanvas
2782 ################################################################################
2783 # graph key
2784 ################################################################################
2787 # g = graph.graphxy(key=graph.key())
2788 # g.addkey(graph.key(), ...)
2791 class key:
2793 def __init__(self, dist="0.2 cm", pos = "tr", hinside = 1, vinside = 1, hdist="0.6 cm", vdist="0.4 cm",
2794 symbolwidth="0.5 cm", symbolheight="0.25 cm", symbolspace="0.2 cm",
2795 textattrs=textmodule.vshift.mathaxis):
2796 self.dist_str = dist
2797 self.pos = pos
2798 self.hinside = hinside
2799 self.vinside = vinside
2800 self.hdist_str = hdist
2801 self.vdist_str = vdist
2802 self.symbolwidth_str = symbolwidth
2803 self.symbolheight_str = symbolheight
2804 self.symbolspace_str = symbolspace
2805 self.textattrs = textattrs
2806 self.plotinfos = None
2807 if self.pos in ("tr", "rt"):
2808 self.right = 1
2809 self.top = 1
2810 elif self.pos in ("br", "rb"):
2811 self.right = 1
2812 self.top = 0
2813 elif self.pos in ("tl", "lt"):
2814 self.right = 0
2815 self.top = 1
2816 elif self.pos in ("bl", "lb"):
2817 self.right = 0
2818 self.top = 0
2819 else:
2820 raise RuntimeError("invalid pos attribute")
2822 def setplotinfos(self, *plotinfos):
2823 """set the plotinfos to be used in the key
2824 - call it exactly once
2825 - plotinfo instances with title == None are ignored"""
2826 if self.plotinfos is not None:
2827 raise RuntimeError("setplotinfo is called multiple times")
2828 self.plotinfos = [plotinfo for plotinfo in plotinfos if plotinfo.data.title is not None]
2830 def dolayout(self, graph):
2831 "creates the layout of the key"
2832 self.dist_pt = unit.topt(unit.length(self.dist_str, default_type="v"))
2833 self.hdist_pt = unit.topt(unit.length(self.hdist_str, default_type="v"))
2834 self.vdist_pt = unit.topt(unit.length(self.vdist_str, default_type="v"))
2835 self.symbolwidth_pt = unit.topt(unit.length(self.symbolwidth_str, default_type="v"))
2836 self.symbolheight_pt = unit.topt(unit.length(self.symbolheight_str, default_type="v"))
2837 self.symbolspace_pt = unit.topt(unit.length(self.symbolspace_str, default_type="v"))
2838 self.titles = []
2839 for plotinfo in self.plotinfos:
2840 self.titles.append(graph.texrunner.text_pt(0, 0, plotinfo.data.title, *helper.ensuresequence(self.textattrs)))
2841 box.tile_pt(self.titles, self.dist_pt, 0, -1)
2842 box.linealignequal_pt(self.titles, self.symbolwidth_pt + self.symbolspace_pt, 1, 0)
2844 def bbox(self):
2845 """return a bbox for the key
2846 method should be called after dolayout"""
2847 result = bbox.bbox()
2848 for title in self.titles:
2849 result = result + title.bbox() + bbox._bbox(0, title.center[1] - 0.5 * self.symbolheight_pt,
2850 0, title.center[1] + 0.5 * self.symbolheight_pt)
2851 return result
2853 def paint(self, c, x, y):
2854 """paint the graph key into a canvas c at the position x and y (in postscript points)
2855 - method should be called after dolayout
2856 - the x, y alignment might be calculated by the graph using:
2857 - the bbox of the key as returned by the keys bbox method
2858 - the attributes hdist_pt, vdist_pt, hinside, and vinside of the key
2859 - the dimension and geometry of the graph"""
2860 sc = c.insert(canvas.canvas(trafomodule._translate(x, y)))
2861 for plotinfo, title in zip(self.plotinfos, self.titles):
2862 plotinfo.style.key(sc, 0, -0.5 * self.symbolheight_pt + title.center[1],
2863 self.symbolwidth_pt, self.symbolheight_pt)
2864 sc.insert(title)
2867 ################################################################################
2868 # graph
2869 ################################################################################
2872 class lineaxispos:
2873 """an axispos linear along a line with a fix direction for the ticks"""
2875 __implements__ = _Iaxispos
2877 def __init__(self, convert, x1, y1, x2, y2, fixtickdirection):
2878 """initializes the instance
2879 - only the convert method is needed from the axis
2880 - x1, y1, x2, y2 are PyX length"""
2881 self.convert = convert
2882 self.x1 = x1
2883 self.y1 = y1
2884 self.x2 = x2
2885 self.y2 = y2
2886 self.x1_pt = unit.topt(x1)
2887 self.y1_pt = unit.topt(y1)
2888 self.x2_pt = unit.topt(x2)
2889 self.y2_pt = unit.topt(y2)
2890 self.fixtickdirection = fixtickdirection
2892 def vbasepath(self, v1=None, v2=None):
2893 if v1 is None:
2894 v1 = 0
2895 if v2 is None:
2896 v2 = 1
2897 return path._line((1-v1)*self.x1_pt+v1*self.x2_pt,
2898 (1-v1)*self.y1_pt+v1*self.y2_pt,
2899 (1-v2)*self.x1_pt+v2*self.x2_pt,
2900 (1-v2)*self.y1_pt+v2*self.y2_pt)
2902 def basepath(self, x1=None, x2=None):
2903 if x1 is None:
2904 v1 = 0
2905 else:
2906 v1 = self.convert(x1)
2907 if x2 is None:
2908 v2 = 1
2909 else:
2910 v2 = self.convert(x2)
2911 return path._line((1-v1)*self.x1_pt+v1*self.x2_pt,
2912 (1-v1)*self.y1_pt+v1*self.y2_pt,
2913 (1-v2)*self.x1_pt+v2*self.x2_pt,
2914 (1-v2)*self.y1_pt+v2*self.y2_pt)
2916 def gridpath(self, x):
2917 raise RuntimeError("gridpath not available")
2919 def vgridpath(self, v):
2920 raise RuntimeError("gridpath not available")
2922 def vtickpoint_pt(self, v):
2923 return (1-v)*self.x1_pt+v*self.x2_pt, (1-v)*self.y1_pt+v*self.y2_pt
2925 def vtickpoint(self, v):
2926 return (1-v)*self.x1+v*self.x2, (1-v)*self.y1+v*self.y2
2928 def tickpoint_pt(self, x):
2929 v = self.convert(x)
2930 return (1-v)*self.x1_pt+v*self.x2_pt, (1-v)*self.y1_pt+v*self.y2_pt
2932 def tickpoint(self, x):
2933 v = self.convert(x)
2934 return (1-v)*self.x1+v*self.x2, (1-v)*self.y1+v*self.y2
2936 def tickdirection(self, x):
2937 return self.fixtickdirection
2939 def vtickdirection(self, v):
2940 return self.fixtickdirection
2943 class lineaxisposlinegrid(lineaxispos):
2944 """an axispos linear along a line with a fix direction for the ticks"""
2946 __implements__ = _Iaxispos
2948 def __init__(self, convert, x1, y1, x2, y2, fixtickdirection, startgridlength, endgridlength):
2949 """initializes the instance
2950 - only the convert method is needed from the axis
2951 - x1, y1, x2, y2 are PyX length"""
2952 lineaxispos.__init__(self, convert, x1, y1, x2, y2, fixtickdirection)
2953 self.startgridlength = startgridlength
2954 self.endgridlength = endgridlength
2955 self.startgridlength_pt = unit.topt(self.startgridlength)
2956 self.endgridlength_pt = unit.topt(self.endgridlength)
2958 def gridpath(self, x):
2959 v = self.convert(x)
2960 return path._line((1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.startgridlength_pt,
2961 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.startgridlength_pt,
2962 (1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.endgridlength_pt,
2963 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.endgridlength_pt)
2965 def vgridpath(self, v):
2966 return path._line((1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.startgridlength_pt,
2967 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.startgridlength_pt,
2968 (1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.endgridlength_pt,
2969 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.endgridlength_pt)
2972 class plotinfo:
2974 def __init__(self, data, style):
2975 self.data = data
2976 self.style = style
2980 class graphxy(canvas.canvas):
2982 Names = "x", "y"
2984 class axisposdata:
2986 def __init__(self, type, axispos, tickdirection):
2988 - type == 0: x-axis; type == 1: y-axis
2989 - axispos_pt is the y or x position of the x-axis or y-axis
2990 in postscript points, respectively
2991 - axispos is analogous to axispos, but as a PyX length
2992 - dx and dy is the tick direction
2994 self.type = type
2995 self.axispos = axispos
2996 self.axispos_pt = unit.topt(axispos)
2997 self.tickdirection = tickdirection
2999 def clipcanvas(self):
3000 return self.insert(canvas.canvas(canvas.clip(path._rect(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt))))
3002 def plot(self, data, style=None):
3003 if self.haslayout:
3004 raise RuntimeError("layout setup was already performed")
3005 if style is None:
3006 if helper.issequence(data):
3007 raise RuntimeError("list plot needs an explicit style")
3008 if self.defaultstyle.has_key(data.defaultstyle):
3009 style = self.defaultstyle[data.defaultstyle].iterate()
3010 else:
3011 style = data.defaultstyle()
3012 self.defaultstyle[data.defaultstyle] = style
3013 plotinfos = []
3014 first = 1
3015 for d in helper.ensuresequence(data):
3016 if not first:
3017 style = style.iterate()
3018 first = 0
3019 if d is not None:
3020 d.setstyle(self, style)
3021 plotinfos.append(plotinfo(d, style))
3022 self.plotinfos.extend(plotinfos)
3023 if helper.issequence(data):
3024 return plotinfos
3025 return plotinfos[0]
3027 def addkey(self, key, *plotinfos):
3028 if self.haslayout:
3029 raise RuntimeError("layout setup was already performed")
3030 self.addkeys.append((key, plotinfos))
3032 def pos_pt(self, x, y, xaxis=None, yaxis=None):
3033 if xaxis is None:
3034 xaxis = self.axes["x"]
3035 if yaxis is None:
3036 yaxis = self.axes["y"]
3037 return self.xpos_pt+xaxis.convert(x)*self.width_pt, self.ypos_pt+yaxis.convert(y)*self.height_pt
3039 def pos(self, x, y, xaxis=None, yaxis=None):
3040 if xaxis is None:
3041 xaxis = self.axes["x"]
3042 if yaxis is None:
3043 yaxis = self.axes["y"]
3044 return self.xpos+xaxis.convert(x)*self.width, self.ypos+yaxis.convert(y)*self.height
3046 def vpos_pt(self, vx, vy):
3047 return self.xpos_pt+vx*self.width_pt, self.ypos_pt+vy*self.height_pt
3049 def vpos(self, vx, vy):
3050 return self.xpos+vx*self.width, self.ypos+vy*self.height
3052 def _addpos(self, x, y, dx, dy):
3053 return x+dx, y+dy
3055 def _connect(self, x1, y1, x2, y2):
3056 return path._lineto(x2, y2)
3058 def keynum(self, key):
3059 try:
3060 while key[0] in string.letters:
3061 key = key[1:]
3062 return int(key)
3063 except IndexError:
3064 return 1
3066 def gatherranges(self):
3067 ranges = {}
3068 for plotinfo in self.plotinfos:
3069 pdranges = plotinfo.data.getranges()
3070 if pdranges is not None:
3071 for key in pdranges.keys():
3072 if key not in ranges.keys():
3073 ranges[key] = pdranges[key]
3074 else:
3075 ranges[key] = (min(ranges[key][0], pdranges[key][0]),
3076 max(ranges[key][1], pdranges[key][1]))
3077 # known ranges are also set as ranges for the axes
3078 for key, axis in self.axes.items():
3079 if key in ranges.keys():
3080 axis.setrange(*ranges[key])
3081 ranges[key] = axis.getrange()
3082 if ranges[key] is None:
3083 del ranges[key]
3084 return ranges
3086 def removedomethod(self, method):
3087 hadmethod = 0
3088 while 1:
3089 try:
3090 self.domethods.remove(method)
3091 hadmethod = 1
3092 except ValueError:
3093 return hadmethod
3095 def dolayout(self):
3096 if not self.removedomethod(self.dolayout): return
3097 self.haslayout = 1
3098 # create list of ranges
3099 # 1. gather ranges
3100 ranges = self.gatherranges()
3101 # 2. calculate additional ranges out of known ranges
3102 for plotinfo in self.plotinfos:
3103 plotinfo.data.setranges(ranges)
3104 # 3. gather ranges again
3105 self.gatherranges()
3106 # do the layout for all axes
3107 axesdist = unit.length(self.axesdist_str, default_type="v")
3108 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3109 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3110 xaxisextents = [0, 0]
3111 yaxisextents = [0, 0]
3112 needxaxisdist = [0, 0]
3113 needyaxisdist = [0, 0]
3114 items = list(self.axes.items())
3115 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3116 for key, axis in items:
3117 num = self.keynum(key)
3118 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3119 num3 = 2 * (num % 2) - 1 # x1 -> 1, x2 -> -1, x3 -> 1, x4 -> -1, ...
3120 if XPattern.match(key):
3121 if needxaxisdist[num2]:
3122 xaxisextents[num2] += axesdist
3123 self.axespos[key] = lineaxisposlinegrid(self.axes[key].convert,
3124 self.xpos,
3125 self.ypos + num2*self.height - num3*xaxisextents[num2],
3126 self.xpos + self.width,
3127 self.ypos + num2*self.height - num3*xaxisextents[num2],
3128 (0, num3),
3129 xaxisextents[num2], xaxisextents[num2] + self.height)
3130 if num == 1:
3131 self.xbasepath = self.axespos[key].basepath
3132 self.xvbasepath = self.axespos[key].vbasepath
3133 self.xgridpath = self.axespos[key].gridpath
3134 self.xvgridpath = self.axespos[key].vgridpath
3135 self.xtickpoint_pt = self.axespos[key].tickpoint_pt
3136 self.xtickpoint = self.axespos[key].tickpoint
3137 self.xvtickpoint_pt = self.axespos[key].vtickpoint_pt
3138 self.xvtickpoint = self.axespos[key].tickpoint
3139 self.xtickdirection = self.axespos[key].tickdirection
3140 self.xvtickdirection = self.axespos[key].vtickdirection
3141 elif YPattern.match(key):
3142 if needyaxisdist[num2]:
3143 yaxisextents[num2] += axesdist
3144 self.axespos[key] = lineaxisposlinegrid(self.axes[key].convert,
3145 self.xpos + num2*self.width - num3*yaxisextents[num2],
3146 self.ypos,
3147 self.xpos + num2*self.width - num3*yaxisextents[num2],
3148 self.ypos + self.height,
3149 (num3, 0),
3150 yaxisextents[num2], yaxisextents[num2] + self.width)
3151 if num == 1:
3152 self.ybasepath = self.axespos[key].basepath
3153 self.yvbasepath = self.axespos[key].vbasepath
3154 self.ygridpath = self.axespos[key].gridpath
3155 self.yvgridpath = self.axespos[key].vgridpath
3156 self.ytickpoint_pt = self.axespos[key].tickpoint_pt
3157 self.ytickpoint = self.axespos[key].tickpoint
3158 self.yvtickpoint_pt = self.axespos[key].vtickpoint_pt
3159 self.yvtickpoint = self.axespos[key].tickpoint
3160 self.ytickdirection = self.axespos[key].tickdirection
3161 self.yvtickdirection = self.axespos[key].vtickdirection
3162 else:
3163 raise ValueError("Axis key '%s' not allowed" % key)
3164 axis.finish(self.axespos[key])
3165 if XPattern.match(key):
3166 xaxisextents[num2] += axis.axiscanvas.extent
3167 needxaxisdist[num2] = 1
3168 if YPattern.match(key):
3169 yaxisextents[num2] += axis.axiscanvas.extent
3170 needyaxisdist[num2] = 1
3172 def dobackground(self):
3173 self.dolayout()
3174 if not self.removedomethod(self.dobackground): return
3175 if self.backgroundattrs is not None:
3176 self.draw(path._rect(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt),
3177 *helper.ensuresequence(self.backgroundattrs))
3179 def doaxes(self):
3180 self.dolayout()
3181 if not self.removedomethod(self.doaxes): return
3182 for axis in self.axes.values():
3183 self.insert(axis.axiscanvas)
3185 def dodata(self):
3186 self.dolayout()
3187 if not self.removedomethod(self.dodata): return
3188 for plotinfo in self.plotinfos:
3189 plotinfo.data.draw(self)
3191 def _dokey(self, key, *plotinfos):
3192 key.setplotinfos(*plotinfos)
3193 key.dolayout(self)
3194 bbox = key.bbox()
3195 if key.right:
3196 if key.hinside:
3197 x = self.xpos_pt + self.width_pt - bbox.urx - key.hdist_pt
3198 else:
3199 x = self.xpos_pt + self.width_pt - bbox.llx + key.hdist_pt
3200 else:
3201 if key.hinside:
3202 x = self.xpos_pt - bbox.llx + key.hdist_pt
3203 else:
3204 x = self.xpos_pt - bbox.urx - key.hdist_pt
3205 if key.top:
3206 if key.vinside:
3207 y = self.ypos_pt + self.height_pt - bbox.ury - key.vdist_pt
3208 else:
3209 y = self.ypos_pt + self.height_pt - bbox.lly + key.vdist_pt
3210 else:
3211 if key.vinside:
3212 y = self.ypos_pt - bbox.lly + key.vdist_pt
3213 else:
3214 y = self.ypos_pt - bbox.ury - key.vdist_pt
3215 key.paint(self, x, y)
3217 def dokey(self):
3218 self.dolayout()
3219 if not self.removedomethod(self.dokey): return
3220 if self.key is not None:
3221 self._dokey(self.key, *self.plotinfos)
3222 for key, plotinfos in self.addkeys:
3223 self._dokey(key, *plotinfos)
3225 def finish(self):
3226 while len(self.domethods):
3227 self.domethods[0]()
3229 def initwidthheight(self, width, height, ratio):
3230 if (width is not None) and (height is None):
3231 self.width = unit.length(width)
3232 self.height = (1.0/ratio) * self.width
3233 elif (height is not None) and (width is None):
3234 self.height = unit.length(height)
3235 self.width = ratio * self.height
3236 else:
3237 self.width = unit.length(width)
3238 self.height = unit.length(height)
3239 self.width_pt = unit.topt(self.width)
3240 self.height_pt = unit.topt(self.height)
3241 if self.width_pt <= 0: raise ValueError("width <= 0")
3242 if self.height_pt <= 0: raise ValueError("height <= 0")
3244 def initaxes(self, axes, addlinkaxes=0):
3245 for key in self.Names:
3246 if not axes.has_key(key):
3247 axes[key] = linaxis()
3248 elif axes[key] is None:
3249 del axes[key]
3250 if addlinkaxes:
3251 if not axes.has_key(key + "2") and axes.has_key(key):
3252 axes[key + "2"] = axes[key].createlinkaxis()
3253 elif axes[key + "2"] is None:
3254 del axes[key + "2"]
3255 self.axes = axes
3257 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
3258 key=None, backgroundattrs=None, axesdist="0.8 cm", **axes):
3259 canvas.canvas.__init__(self)
3260 self.xpos = unit.length(xpos)
3261 self.ypos = unit.length(ypos)
3262 self.xpos_pt = unit.topt(self.xpos)
3263 self.ypos_pt = unit.topt(self.ypos)
3264 self.initwidthheight(width, height, ratio)
3265 self.initaxes(axes, 1)
3266 self.axescanvas = {}
3267 self.axespos = {}
3268 self.key = key
3269 self.backgroundattrs = backgroundattrs
3270 self.axesdist_str = axesdist
3271 self.plotinfos = []
3272 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata, self.dokey]
3273 self.haslayout = 0
3274 self.defaultstyle = {}
3275 self.addkeys = []
3277 def bbox(self):
3278 self.finish()
3279 return canvas.canvas.bbox(self)
3281 def write(self, file):
3282 self.finish()
3283 canvas.canvas.write(self, file)
3287 # some thoughts, but deferred right now
3289 # class graphxyz(graphxy):
3291 # Names = "x", "y", "z"
3293 # def _vxtickpoint(self, axis, v):
3294 # return self._vpos(v, axis.vypos, axis.vzpos)
3296 # def _vytickpoint(self, axis, v):
3297 # return self._vpos(axis.vxpos, v, axis.vzpos)
3299 # def _vztickpoint(self, axis, v):
3300 # return self._vpos(axis.vxpos, axis.vypos, v)
3302 # def vxtickdirection(self, axis, v):
3303 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
3304 # x2, y2 = self._vpos(v, 0.5, 0)
3305 # dx, dy = x1 - x2, y1 - y2
3306 # norm = math.sqrt(dx*dx + dy*dy)
3307 # return dx/norm, dy/norm
3309 # def vytickdirection(self, axis, v):
3310 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
3311 # x2, y2 = self._vpos(0.5, v, 0)
3312 # dx, dy = x1 - x2, y1 - y2
3313 # norm = math.sqrt(dx*dx + dy*dy)
3314 # return dx/norm, dy/norm
3316 # def vztickdirection(self, axis, v):
3317 # return -1, 0
3318 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
3319 # x2, y2 = self._vpos(0.5, 0.5, v)
3320 # dx, dy = x1 - x2, y1 - y2
3321 # norm = math.sqrt(dx*dx + dy*dy)
3322 # return dx/norm, dy/norm
3324 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3325 # if xaxis is None: xaxis = self.axes["x"]
3326 # if yaxis is None: yaxis = self.axes["y"]
3327 # if zaxis is None: zaxis = self.axes["z"]
3328 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3330 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3331 # if xaxis is None: xaxis = self.axes["x"]
3332 # if yaxis is None: yaxis = self.axes["y"]
3333 # if zaxis is None: zaxis = self.axes["z"]
3334 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3336 # def _vpos(self, vx, vy, vz):
3337 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
3338 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
3339 # + self.a[2]*self.b[0]*(y-self.eye[1])
3340 # + self.a[1]*self.b[2]*(x-self.eye[0])
3341 # - self.a[2]*self.b[1]*(x-self.eye[0])
3342 # - self.a[0]*self.b[2]*(y-self.eye[1])
3343 # - self.a[1]*self.b[0]*(z-self.eye[2]))
3344 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
3345 # + self.eye[2]*self.b[0]*(y-self.eye[1])
3346 # + self.eye[1]*self.b[2]*(x-self.eye[0])
3347 # - self.eye[2]*self.b[1]*(x-self.eye[0])
3348 # - self.eye[0]*self.b[2]*(y-self.eye[1])
3349 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
3350 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
3351 # + self.a[2]*self.eye[0]*(y-self.eye[1])
3352 # + self.a[1]*self.eye[2]*(x-self.eye[0])
3353 # - self.a[2]*self.eye[1]*(x-self.eye[0])
3354 # - self.a[0]*self.eye[2]*(y-self.eye[1])
3355 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
3356 # return da/d0 + self._xpos, db/d0 + self._ypos
3358 # def vpos(self, vx, vy, vz):
3359 # tx, ty = self._vpos(vx, vy, vz)
3360 # return unit.t_pt(tx), unit.t_pt(ty)
3362 # def xbaseline(self, axis, x1, x2, xaxis=None):
3363 # if xaxis is None: xaxis = self.axes["x"]
3364 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2))
3366 # def ybaseline(self, axis, y1, y2, yaxis=None):
3367 # if yaxis is None: yaxis = self.axes["y"]
3368 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2))
3370 # def zbaseline(self, axis, z1, z2, zaxis=None):
3371 # if zaxis is None: zaxis = self.axes["z"]
3372 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2))
3374 # def vxbaseline(self, axis, v1, v2):
3375 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
3376 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
3377 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
3378 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
3380 # def vybaseline(self, axis, v1, v2):
3381 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
3382 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
3383 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
3384 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
3386 # def vzbaseline(self, axis, v1, v2):
3387 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
3388 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
3389 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
3390 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
3392 # def xgridpath(self, x, xaxis=None):
3393 # assert 0
3394 # if xaxis is None: xaxis = self.axes["x"]
3395 # v = xaxis.convert(x)
3396 # return path._line(self._xpos+v*self._width, self._ypos,
3397 # self._xpos+v*self._width, self._ypos+self._height)
3399 # def ygridpath(self, y, yaxis=None):
3400 # assert 0
3401 # if yaxis is None: yaxis = self.axes["y"]
3402 # v = yaxis.convert(y)
3403 # return path._line(self._xpos, self._ypos+v*self._height,
3404 # self._xpos+self._width, self._ypos+v*self._height)
3406 # def zgridpath(self, z, zaxis=None):
3407 # assert 0
3408 # if zaxis is None: zaxis = self.axes["z"]
3409 # v = zaxis.convert(z)
3410 # return path._line(self._xpos, self._zpos+v*self._height,
3411 # self._xpos+self._width, self._zpos+v*self._height)
3413 # def vxgridpath(self, v):
3414 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
3415 # path._lineto(*self._vpos(v, 0, 1)),
3416 # path._lineto(*self._vpos(v, 1, 1)),
3417 # path._lineto(*self._vpos(v, 1, 0)),
3418 # path.closepath())
3420 # def vygridpath(self, v):
3421 # return path.path(path._moveto(*self._vpos(0, v, 0)),
3422 # path._lineto(*self._vpos(0, v, 1)),
3423 # path._lineto(*self._vpos(1, v, 1)),
3424 # path._lineto(*self._vpos(1, v, 0)),
3425 # path.closepath())
3427 # def vzgridpath(self, v):
3428 # return path.path(path._moveto(*self._vpos(0, 0, v)),
3429 # path._lineto(*self._vpos(0, 1, v)),
3430 # path._lineto(*self._vpos(1, 1, v)),
3431 # path._lineto(*self._vpos(1, 0, v)),
3432 # path.closepath())
3434 # def _addpos(self, x, y, dx, dy):
3435 # assert 0
3436 # return x+dx, y+dy
3438 # def _connect(self, x1, y1, x2, y2):
3439 # assert 0
3440 # return path._lineto(x2, y2)
3442 # def doaxes(self):
3443 # self.dolayout()
3444 # if not self.removedomethod(self.doaxes): return
3445 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
3446 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3447 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3448 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[2])
3449 # items = list(self.axes.items())
3450 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3451 # for key, axis in items:
3452 # num = self.keynum(key)
3453 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3454 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3455 # if XPattern.match(key):
3456 # axis.vypos = 0
3457 # axis.vzpos = 0
3458 # axis._vtickpoint = self._vxtickpoint
3459 # axis.vgridpath = self.vxgridpath
3460 # axis.vbaseline = self.vxbaseline
3461 # axis.vtickdirection = self.vxtickdirection
3462 # elif YPattern.match(key):
3463 # axis.vxpos = 0
3464 # axis.vzpos = 0
3465 # axis._vtickpoint = self._vytickpoint
3466 # axis.vgridpath = self.vygridpath
3467 # axis.vbaseline = self.vybaseline
3468 # axis.vtickdirection = self.vytickdirection
3469 # elif ZPattern.match(key):
3470 # axis.vxpos = 0
3471 # axis.vypos = 0
3472 # axis._vtickpoint = self._vztickpoint
3473 # axis.vgridpath = self.vzgridpath
3474 # axis.vbaseline = self.vzbaseline
3475 # axis.vtickdirection = self.vztickdirection
3476 # else:
3477 # raise ValueError("Axis key '%s' not allowed" % key)
3478 # if axis.painter is not None:
3479 # axis.dopaint(self)
3480 # # if XPattern.match(key):
3481 # # self._xaxisextents[num2] += axis._extent
3482 # # needxaxisdist[num2] = 1
3483 # # if YPattern.match(key):
3484 # # self._yaxisextents[num2] += axis._extent
3485 # # needyaxisdist[num2] = 1
3487 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
3488 # phi=30, theta=30, distance=1,
3489 # backgroundattrs=None, axesdist="0.8 cm", **axes):
3490 # canvas.canvas.__init__(self)
3491 # self.tex = tex
3492 # self.xpos = xpos
3493 # self.ypos = ypos
3494 # self._xpos = unit.topt(xpos)
3495 # self._ypos = unit.topt(ypos)
3496 # self._width = unit.topt(width)
3497 # self._height = unit.topt(height)
3498 # self._depth = unit.topt(depth)
3499 # self.width = width
3500 # self.height = height
3501 # self.depth = depth
3502 # if self._width <= 0: raise ValueError("width < 0")
3503 # if self._height <= 0: raise ValueError("height < 0")
3504 # if self._depth <= 0: raise ValueError("height < 0")
3505 # self._distance = distance*math.sqrt(self._width*self._width+
3506 # self._height*self._height+
3507 # self._depth*self._depth)
3508 # phi *= -math.pi/180
3509 # theta *= math.pi/180
3510 # self.a = (-math.sin(phi), math.cos(phi), 0)
3511 # self.b = (-math.cos(phi)*math.sin(theta),
3512 # -math.sin(phi)*math.sin(theta),
3513 # math.cos(theta))
3514 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
3515 # self._distance*math.sin(phi)*math.cos(theta),
3516 # self._distance*math.sin(theta))
3517 # self.initaxes(axes)
3518 # self.axesdist_str = axesdist
3519 # self.backgroundattrs = backgroundattrs
3521 # self.data = []
3522 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
3523 # self.haslayout = 0
3524 # self.defaultstyle = {}
3526 # def bbox(self):
3527 # self.finish()
3528 # return bbox._bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
3531 ################################################################################
3532 # attr changers
3533 ################################################################################
3536 #class _Ichangeattr:
3537 # """attribute changer
3538 # is an iterator for attributes where an attribute
3539 # is not refered by just a number (like for a sequence),
3540 # but also by the number of attributes requested
3541 # by calls of the next method (like for an color palette)
3542 # (you should ensure to call all needed next before the attr)
3544 # the attribute itself is implemented by overloading the _attr method"""
3546 # def attr(self):
3547 # "get an attribute"
3549 # def next(self):
3550 # "get an attribute changer for the next attribute"
3553 class _changeattr: pass
3556 class changeattr(_changeattr):
3558 def __init__(self):
3559 self.counter = 1
3561 def getattr(self):
3562 return self.attr(0)
3564 def iterate(self):
3565 newindex = self.counter
3566 self.counter += 1
3567 return refattr(self, newindex)
3570 class refattr(_changeattr):
3572 def __init__(self, ref, index):
3573 self.ref = ref
3574 self.index = index
3576 def getattr(self):
3577 return self.ref.attr(self.index)
3579 def iterate(self):
3580 return self.ref.iterate()
3583 # helper routines for a using attrs
3585 def _getattr(attr):
3586 "get attr out of a attr/changeattr"
3587 if isinstance(attr, _changeattr):
3588 return attr.getattr()
3589 return attr
3592 def _getattrs(attrs):
3593 "get attrs out of a list of attr/changeattr"
3594 if attrs is not None:
3595 result = []
3596 for attr in helper.ensuresequence(attrs):
3597 if isinstance(attr, _changeattr):
3598 attr = attr.getattr()
3599 if attr is not None:
3600 result.append(attr)
3601 if len(result) or not len(attrs):
3602 return result
3605 def _iterateattr(attr):
3606 "perform next to a attr/changeattr"
3607 if isinstance(attr, _changeattr):
3608 return attr.iterate()
3609 return attr
3612 def _iterateattrs(attrs):
3613 "perform next to a list of attr/changeattr"
3614 if attrs is not None:
3615 result = []
3616 for attr in helper.ensuresequence(attrs):
3617 if isinstance(attr, _changeattr):
3618 result.append(attr.iterate())
3619 else:
3620 result.append(attr)
3621 return result
3624 class changecolor(changeattr):
3626 def __init__(self, palette):
3627 changeattr.__init__(self)
3628 self.palette = palette
3630 def attr(self, index):
3631 if self.counter != 1:
3632 return self.palette.getcolor(index/float(self.counter-1))
3633 else:
3634 return self.palette.getcolor(0)
3637 class _changecolorgray(changecolor):
3639 def __init__(self, palette=color.palette.Gray):
3640 changecolor.__init__(self, palette)
3642 _changecolorgrey = _changecolorgray
3645 class _changecolorreversegray(changecolor):
3647 def __init__(self, palette=color.palette.ReverseGray):
3648 changecolor.__init__(self, palette)
3650 _changecolorreversegrey = _changecolorreversegray
3653 class _changecolorredblack(changecolor):
3655 def __init__(self, palette=color.palette.RedBlack):
3656 changecolor.__init__(self, palette)
3659 class _changecolorblackred(changecolor):
3661 def __init__(self, palette=color.palette.BlackRed):
3662 changecolor.__init__(self, palette)
3665 class _changecolorredwhite(changecolor):
3667 def __init__(self, palette=color.palette.RedWhite):
3668 changecolor.__init__(self, palette)
3671 class _changecolorwhitered(changecolor):
3673 def __init__(self, palette=color.palette.WhiteRed):
3674 changecolor.__init__(self, palette)
3677 class _changecolorgreenblack(changecolor):
3679 def __init__(self, palette=color.palette.GreenBlack):
3680 changecolor.__init__(self, palette)
3683 class _changecolorblackgreen(changecolor):
3685 def __init__(self, palette=color.palette.BlackGreen):
3686 changecolor.__init__(self, palette)
3689 class _changecolorgreenwhite(changecolor):
3691 def __init__(self, palette=color.palette.GreenWhite):
3692 changecolor.__init__(self, palette)
3695 class _changecolorwhitegreen(changecolor):
3697 def __init__(self, palette=color.palette.WhiteGreen):
3698 changecolor.__init__(self, palette)
3701 class _changecolorblueblack(changecolor):
3703 def __init__(self, palette=color.palette.BlueBlack):
3704 changecolor.__init__(self, palette)
3707 class _changecolorblackblue(changecolor):
3709 def __init__(self, palette=color.palette.BlackBlue):
3710 changecolor.__init__(self, palette)
3713 class _changecolorbluewhite(changecolor):
3715 def __init__(self, palette=color.palette.BlueWhite):
3716 changecolor.__init__(self, palette)
3719 class _changecolorwhiteblue(changecolor):
3721 def __init__(self, palette=color.palette.WhiteBlue):
3722 changecolor.__init__(self, palette)
3725 class _changecolorredgreen(changecolor):
3727 def __init__(self, palette=color.palette.RedGreen):
3728 changecolor.__init__(self, palette)
3731 class _changecolorredblue(changecolor):
3733 def __init__(self, palette=color.palette.RedBlue):
3734 changecolor.__init__(self, palette)
3737 class _changecolorgreenred(changecolor):
3739 def __init__(self, palette=color.palette.GreenRed):
3740 changecolor.__init__(self, palette)
3743 class _changecolorgreenblue(changecolor):
3745 def __init__(self, palette=color.palette.GreenBlue):
3746 changecolor.__init__(self, palette)
3749 class _changecolorbluered(changecolor):
3751 def __init__(self, palette=color.palette.BlueRed):
3752 changecolor.__init__(self, palette)
3755 class _changecolorbluegreen(changecolor):
3757 def __init__(self, palette=color.palette.BlueGreen):
3758 changecolor.__init__(self, palette)
3761 class _changecolorrainbow(changecolor):
3763 def __init__(self, palette=color.palette.Rainbow):
3764 changecolor.__init__(self, palette)
3767 class _changecolorreverserainbow(changecolor):
3769 def __init__(self, palette=color.palette.ReverseRainbow):
3770 changecolor.__init__(self, palette)
3773 class _changecolorhue(changecolor):
3775 def __init__(self, palette=color.palette.Hue):
3776 changecolor.__init__(self, palette)
3779 class _changecolorreversehue(changecolor):
3781 def __init__(self, palette=color.palette.ReverseHue):
3782 changecolor.__init__(self, palette)
3785 changecolor.Gray = _changecolorgray
3786 changecolor.Grey = _changecolorgrey
3787 changecolor.Reversegray = _changecolorreversegray
3788 changecolor.Reversegrey = _changecolorreversegrey
3789 changecolor.RedBlack = _changecolorredblack
3790 changecolor.BlackRed = _changecolorblackred
3791 changecolor.RedWhite = _changecolorredwhite
3792 changecolor.WhiteRed = _changecolorwhitered
3793 changecolor.GreenBlack = _changecolorgreenblack
3794 changecolor.BlackGreen = _changecolorblackgreen
3795 changecolor.GreenWhite = _changecolorgreenwhite
3796 changecolor.WhiteGreen = _changecolorwhitegreen
3797 changecolor.BlueBlack = _changecolorblueblack
3798 changecolor.BlackBlue = _changecolorblackblue
3799 changecolor.BlueWhite = _changecolorbluewhite
3800 changecolor.WhiteBlue = _changecolorwhiteblue
3801 changecolor.RedGreen = _changecolorredgreen
3802 changecolor.RedBlue = _changecolorredblue
3803 changecolor.GreenRed = _changecolorgreenred
3804 changecolor.GreenBlue = _changecolorgreenblue
3805 changecolor.BlueRed = _changecolorbluered
3806 changecolor.BlueGreen = _changecolorbluegreen
3807 changecolor.Rainbow = _changecolorrainbow
3808 changecolor.ReverseRainbow = _changecolorreverserainbow
3809 changecolor.Hue = _changecolorhue
3810 changecolor.ReverseHue = _changecolorreversehue
3813 class changesequence(changeattr):
3814 "cycles through a list"
3816 def __init__(self, *sequence):
3817 changeattr.__init__(self)
3818 if not len(sequence):
3819 sequence = self.defaultsequence
3820 self.sequence = sequence
3822 def attr(self, index):
3823 return self.sequence[index % len(self.sequence)]
3826 class changelinestyle(changesequence):
3827 defaultsequence = (style.linestyle.solid,
3828 style.linestyle.dashed,
3829 style.linestyle.dotted,
3830 style.linestyle.dashdotted)
3833 class changestrokedfilled(changesequence):
3834 defaultsequence = (deco.stroked(), deco.filled())
3837 class changefilledstroked(changesequence):
3838 defaultsequence = (deco.filled(), deco.stroked())
3842 ################################################################################
3843 # styles
3844 ################################################################################
3847 class symbol:
3849 def cross(self, x, y):
3850 return (path._moveto(x-0.5*self.size_pt, y-0.5*self.size_pt),
3851 path._lineto(x+0.5*self.size_pt, y+0.5*self.size_pt),
3852 path._moveto(x-0.5*self.size_pt, y+0.5*self.size_pt),
3853 path._lineto(x+0.5*self.size_pt, y-0.5*self.size_pt))
3855 def plus(self, x, y):
3856 return (path._moveto(x-0.707106781*self.size_pt, y),
3857 path._lineto(x+0.707106781*self.size_pt, y),
3858 path._moveto(x, y-0.707106781*self.size_pt),
3859 path._lineto(x, y+0.707106781*self.size_pt))
3861 def square(self, x, y):
3862 return (path._moveto(x-0.5*self.size_pt, y-0.5 * self.size_pt),
3863 path._lineto(x+0.5*self.size_pt, y-0.5 * self.size_pt),
3864 path._lineto(x+0.5*self.size_pt, y+0.5 * self.size_pt),
3865 path._lineto(x-0.5*self.size_pt, y+0.5 * self.size_pt),
3866 path.closepath())
3868 def triangle(self, x, y):
3869 return (path._moveto(x-0.759835685*self.size_pt, y-0.438691337*self.size_pt),
3870 path._lineto(x+0.759835685*self.size_pt, y-0.438691337*self.size_pt),
3871 path._lineto(x, y+0.877382675*self.size_pt),
3872 path.closepath())
3874 def circle(self, x, y):
3875 return (path._arc(x, y, 0.564189583*self.size_pt, 0, 360),
3876 path.closepath())
3878 def diamond(self, x, y):
3879 return (path._moveto(x-0.537284965*self.size_pt, y),
3880 path._lineto(x, y-0.930604859*self.size_pt),
3881 path._lineto(x+0.537284965*self.size_pt, y),
3882 path._lineto(x, y+0.930604859*self.size_pt),
3883 path.closepath())
3885 def __init__(self, symbol=helper.nodefault,
3886 size="0.2 cm", symbolattrs=deco.stroked(),
3887 errorscale=0.5, errorbarattrs=(),
3888 lineattrs=None):
3889 self.size_str = size
3890 if symbol is helper.nodefault:
3891 self._symbol = changesymbol.cross()
3892 else:
3893 self._symbol = symbol
3894 self._symbolattrs = symbolattrs
3895 self.errorscale = errorscale
3896 self._errorbarattrs = errorbarattrs
3897 self._lineattrs = lineattrs
3899 def iteratedict(self):
3900 result = {}
3901 result["symbol"] = _iterateattr(self._symbol)
3902 result["size"] = _iterateattr(self.size_str)
3903 result["symbolattrs"] = _iterateattrs(self._symbolattrs)
3904 result["errorscale"] = _iterateattr(self.errorscale)
3905 result["errorbarattrs"] = _iterateattrs(self._errorbarattrs)
3906 result["lineattrs"] = _iterateattrs(self._lineattrs)
3907 return result
3909 def iterate(self):
3910 return symbol(**self.iteratedict())
3912 def othercolumnkey(self, key, index):
3913 raise ValueError("unsuitable key '%s'" % key)
3915 def setcolumns(self, graph, columns):
3916 def checkpattern(key, index, pattern, iskey, isindex):
3917 if key is not None:
3918 match = pattern.match(key)
3919 if match:
3920 if isindex is not None: raise ValueError("multiple key specification")
3921 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
3922 key = None
3923 iskey = match.groups()[0]
3924 isindex = index
3925 return key, iskey, isindex
3927 self.xi = self.xmini = self.xmaxi = None
3928 self.dxi = self.dxmini = self.dxmaxi = None
3929 self.yi = self.ymini = self.ymaxi = None
3930 self.dyi = self.dymini = self.dymaxi = None
3931 self.xkey = self.ykey = None
3932 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
3933 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3934 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3935 XMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
3936 YMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
3937 XMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
3938 YMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
3939 DXPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
3940 DYPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
3941 DXMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
3942 DYMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
3943 DXMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
3944 DYMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
3945 for key, index in columns.items():
3946 key, self.xkey, self.xi = checkpattern(key, index, XPattern, self.xkey, self.xi)
3947 key, self.ykey, self.yi = checkpattern(key, index, YPattern, self.ykey, self.yi)
3948 key, self.xkey, self.xmini = checkpattern(key, index, XMinPattern, self.xkey, self.xmini)
3949 key, self.ykey, self.ymini = checkpattern(key, index, YMinPattern, self.ykey, self.ymini)
3950 key, self.xkey, self.xmaxi = checkpattern(key, index, XMaxPattern, self.xkey, self.xmaxi)
3951 key, self.ykey, self.ymaxi = checkpattern(key, index, YMaxPattern, self.ykey, self.ymaxi)
3952 key, self.xkey, self.dxi = checkpattern(key, index, DXPattern, self.xkey, self.dxi)
3953 key, self.ykey, self.dyi = checkpattern(key, index, DYPattern, self.ykey, self.dyi)
3954 key, self.xkey, self.dxmini = checkpattern(key, index, DXMinPattern, self.xkey, self.dxmini)
3955 key, self.ykey, self.dymini = checkpattern(key, index, DYMinPattern, self.ykey, self.dymini)
3956 key, self.xkey, self.dxmaxi = checkpattern(key, index, DXMaxPattern, self.xkey, self.dxmaxi)
3957 key, self.ykey, self.dymaxi = checkpattern(key, index, DYMaxPattern, self.ykey, self.dymaxi)
3958 if key is not None:
3959 self.othercolumnkey(key, index)
3960 if None in (self.xkey, self.ykey): raise ValueError("incomplete axis specification")
3961 if (len(filter(None, (self.xmini, self.dxmini, self.dxi))) > 1 or
3962 len(filter(None, (self.ymini, self.dymini, self.dyi))) > 1 or
3963 len(filter(None, (self.xmaxi, self.dxmaxi, self.dxi))) > 1 or
3964 len(filter(None, (self.ymaxi, self.dymaxi, self.dyi))) > 1):
3965 raise ValueError("multiple errorbar definition")
3966 if ((self.xi is None and self.dxi is not None) or
3967 (self.yi is None and self.dyi is not None) or
3968 (self.xi is None and self.dxmini is not None) or
3969 (self.yi is None and self.dymini is not None) or
3970 (self.xi is None and self.dxmaxi is not None) or
3971 (self.yi is None and self.dymaxi is not None)):
3972 raise ValueError("errorbar definition start value missing")
3973 self.xaxis = graph.axes[self.xkey]
3974 self.yaxis = graph.axes[self.ykey]
3976 def minmidmax(self, point, i, mini, maxi, di, dmini, dmaxi):
3977 min = max = mid = None
3978 try:
3979 mid = point[i] + 0.0
3980 except (TypeError, ValueError):
3981 pass
3982 try:
3983 if di is not None: min = point[i] - point[di]
3984 elif dmini is not None: min = point[i] - point[dmini]
3985 elif mini is not None: min = point[mini] + 0.0
3986 except (TypeError, ValueError):
3987 pass
3988 try:
3989 if di is not None: max = point[i] + point[di]
3990 elif dmaxi is not None: max = point[i] + point[dmaxi]
3991 elif maxi is not None: max = point[maxi] + 0.0
3992 except (TypeError, ValueError):
3993 pass
3994 if mid is not None:
3995 if min is not None and min > mid: raise ValueError("minimum error in errorbar")
3996 if max is not None and max < mid: raise ValueError("maximum error in errorbar")
3997 else:
3998 if min is not None and max is not None and min > max: raise ValueError("minimum/maximum error in errorbar")
3999 return min, mid, max
4001 def keyrange(self, points, i, mini, maxi, di, dmini, dmaxi):
4002 allmin = allmax = None
4003 if filter(None, (mini, maxi, di, dmini, dmaxi)) is not None:
4004 for point in points:
4005 min, mid, max = self.minmidmax(point, i, mini, maxi, di, dmini, dmaxi)
4006 if min is not None and (allmin is None or min < allmin): allmin = min
4007 if mid is not None and (allmin is None or mid < allmin): allmin = mid
4008 if mid is not None and (allmax is None or mid > allmax): allmax = mid
4009 if max is not None and (allmax is None or max > allmax): allmax = max
4010 else:
4011 for point in points:
4012 try:
4013 value = point[i] + 0.0
4014 if allmin is None or point[i] < allmin: allmin = point[i]
4015 if allmax is None or point[i] > allmax: allmax = point[i]
4016 except (TypeError, ValueError):
4017 pass
4018 return allmin, allmax
4020 def getranges(self, points):
4021 xmin, xmax = self.keyrange(points, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4022 ymin, ymax = self.keyrange(points, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4023 return {self.xkey: (xmin, xmax), self.ykey: (ymin, ymax)}
4025 def drawerrorbar_pt(self, graph, topleft, top, topright,
4026 left, center, right,
4027 bottomleft, bottom, bottomright, point=None):
4028 if left is not None:
4029 if right is not None:
4030 left1 = graph._addpos(*(left+(0, -self.errorsize_pt)))
4031 left2 = graph._addpos(*(left+(0, self.errorsize_pt)))
4032 right1 = graph._addpos(*(right+(0, -self.errorsize_pt)))
4033 right2 = graph._addpos(*(right+(0, self.errorsize_pt)))
4034 graph.stroke(path.path(path._moveto(*left1),
4035 graph._connect(*(left1+left2)),
4036 path._moveto(*left),
4037 graph._connect(*(left+right)),
4038 path._moveto(*right1),
4039 graph._connect(*(right1+right2))),
4040 *self.errorbarattrs)
4041 elif center is not None:
4042 left1 = graph._addpos(*(left+(0, -self.errorsize_pt)))
4043 left2 = graph._addpos(*(left+(0, self.errorsize_pt)))
4044 graph.stroke(path.path(path._moveto(*left1),
4045 graph._connect(*(left1+left2)),
4046 path._moveto(*left),
4047 graph._connect(*(left+center))),
4048 *self.errorbarattrs)
4049 else:
4050 left1 = graph._addpos(*(left+(0, -self.errorsize_pt)))
4051 left2 = graph._addpos(*(left+(0, self.errorsize_pt)))
4052 left3 = graph._addpos(*(left+(self.errorsize_pt, 0)))
4053 graph.stroke(path.path(path._moveto(*left1),
4054 graph._connect(*(left1+left2)),
4055 path._moveto(*left),
4056 graph._connect(*(left+left3))),
4057 *self.errorbarattrs)
4058 if right is not None and left is None:
4059 if center is not None:
4060 right1 = graph._addpos(*(right+(0, -self.errorsize_pt)))
4061 right2 = graph._addpos(*(right+(0, self.errorsize_pt)))
4062 graph.stroke(path.path(path._moveto(*right1),
4063 graph._connect(*(right1+right2)),
4064 path._moveto(*right),
4065 graph._connect(*(right+center))),
4066 *self.errorbarattrs)
4067 else:
4068 right1 = graph._addpos(*(right+(0, -self.errorsize_pt)))
4069 right2 = graph._addpos(*(right+(0, self.errorsize_pt)))
4070 right3 = graph._addpos(*(right+(-self.errorsize_pt, 0)))
4071 graph.stroke(path.path(path._moveto(*right1),
4072 graph._connect(*(right1+right2)),
4073 path._moveto(*right),
4074 graph._connect(*(right+right3))),
4075 *self.errorbarattrs)
4077 if bottom is not None:
4078 if top is not None:
4079 bottom1 = graph._addpos(*(bottom+(-self.errorsize_pt, 0)))
4080 bottom2 = graph._addpos(*(bottom+(self.errorsize_pt, 0)))
4081 top1 = graph._addpos(*(top+(-self.errorsize_pt, 0)))
4082 top2 = graph._addpos(*(top+(self.errorsize_pt, 0)))
4083 graph.stroke(path.path(path._moveto(*bottom1),
4084 graph._connect(*(bottom1+bottom2)),
4085 path._moveto(*bottom),
4086 graph._connect(*(bottom+top)),
4087 path._moveto(*top1),
4088 graph._connect(*(top1+top2))),
4089 *self.errorbarattrs)
4090 elif center is not None:
4091 bottom1 = graph._addpos(*(bottom+(-self.errorsize_pt, 0)))
4092 bottom2 = graph._addpos(*(bottom+(self.errorsize_pt, 0)))
4093 graph.stroke(path.path(path._moveto(*bottom1),
4094 graph._connect(*(bottom1+bottom2)),
4095 path._moveto(*bottom),
4096 graph._connect(*(bottom+center))),
4097 *self.errorbarattrs)
4098 else:
4099 bottom1 = graph._addpos(*(bottom+(-self.errorsize_pt, 0)))
4100 bottom2 = graph._addpos(*(bottom+(self.errorsize_pt, 0)))
4101 bottom3 = graph._addpos(*(bottom+(0, self.errorsize_pt)))
4102 graph.stroke(path.path(path._moveto(*bottom1),
4103 graph._connect(*(bottom1+bottom2)),
4104 path._moveto(*bottom),
4105 graph._connect(*(bottom+bottom3))),
4106 *self.errorbarattrs)
4107 if top is not None and bottom is None:
4108 if center is not None:
4109 top1 = graph._addpos(*(top+(-self.errorsize_pt, 0)))
4110 top2 = graph._addpos(*(top+(self.errorsize_pt, 0)))
4111 graph.stroke(path.path(path._moveto(*top1),
4112 graph._connect(*(top1+top2)),
4113 path._moveto(*top),
4114 graph._connect(*(top+center))),
4115 *self.errorbarattrs)
4116 else:
4117 top1 = graph._addpos(*(top+(-self.errorsize_pt, 0)))
4118 top2 = graph._addpos(*(top+(self.errorsize_pt, 0)))
4119 top3 = graph._addpos(*(top+(0, -self.errorsize_pt)))
4120 graph.stroke(path.path(path._moveto(*top1),
4121 graph._connect(*(top1+top2)),
4122 path._moveto(*top),
4123 graph._connect(*(top+top3))),
4124 *self.errorbarattrs)
4125 if bottomleft is not None:
4126 if topleft is not None and bottomright is None:
4127 bottomleft1 = graph._addpos(*(bottomleft+(self.errorsize_pt, 0)))
4128 topleft1 = graph._addpos(*(topleft+(self.errorsize_pt, 0)))
4129 graph.stroke(path.path(path._moveto(*bottomleft1),
4130 graph._connect(*(bottomleft1+bottomleft)),
4131 graph._connect(*(bottomleft+topleft)),
4132 graph._connect(*(topleft+topleft1))),
4133 *self.errorbarattrs)
4134 elif bottomright is not None and topleft is None:
4135 bottomleft1 = graph._addpos(*(bottomleft+(0, self.errorsize_pt)))
4136 bottomright1 = graph._addpos(*(bottomright+(0, self.errorsize_pt)))
4137 graph.stroke(path.path(path._moveto(*bottomleft1),
4138 graph._connect(*(bottomleft1+bottomleft)),
4139 graph._connect(*(bottomleft+bottomright)),
4140 graph._connect(*(bottomright+bottomright1))),
4141 *self.errorbarattrs)
4142 elif bottomright is None and topleft is None:
4143 bottomleft1 = graph._addpos(*(bottomleft+(self.errorsize_pt, 0)))
4144 bottomleft2 = graph._addpos(*(bottomleft+(0, self.errorsize_pt)))
4145 graph.stroke(path.path(path._moveto(*bottomleft1),
4146 graph._connect(*(bottomleft1+bottomleft)),
4147 graph._connect(*(bottomleft+bottomleft2))),
4148 *self.errorbarattrs)
4149 if topright is not None:
4150 if bottomright is not None and topleft is None:
4151 topright1 = graph._addpos(*(topright+(-self.errorsize_pt, 0)))
4152 bottomright1 = graph._addpos(*(bottomright+(-self.errorsize_pt, 0)))
4153 graph.stroke(path.path(path._moveto(*topright1),
4154 graph._connect(*(topright1+topright)),
4155 graph._connect(*(topright+bottomright)),
4156 graph._connect(*(bottomright+bottomright1))),
4157 *self.errorbarattrs)
4158 elif topleft is not None and bottomright is None:
4159 topright1 = graph._addpos(*(topright+(0, -self.errorsize_pt)))
4160 topleft1 = graph._addpos(*(topleft+(0, -self.errorsize_pt)))
4161 graph.stroke(path.path(path._moveto(*topright1),
4162 graph._connect(*(topright1+topright)),
4163 graph._connect(*(topright+topleft)),
4164 graph._connect(*(topleft+topleft1))),
4165 *self.errorbarattrs)
4166 elif topleft is None and bottomright is None:
4167 topright1 = graph._addpos(*(topright+(-self.errorsize_pt, 0)))
4168 topright2 = graph._addpos(*(topright+(0, -self.errorsize_pt)))
4169 graph.stroke(path.path(path._moveto(*topright1),
4170 graph._connect(*(topright1+topright)),
4171 graph._connect(*(topright+topright2))),
4172 *self.errorbarattrs)
4173 if bottomright is not None and bottomleft is None and topright is None:
4174 bottomright1 = graph._addpos(*(bottomright+(-self.errorsize_pt, 0)))
4175 bottomright2 = graph._addpos(*(bottomright+(0, self.errorsize_pt)))
4176 graph.stroke(path.path(path._moveto(*bottomright1),
4177 graph._connect(*(bottomright1+bottomright)),
4178 graph._connect(*(bottomright+bottomright2))),
4179 *self.errorbarattrs)
4180 if topleft is not None and bottomleft is None and topright is None:
4181 topleft1 = graph._addpos(*(topleft+(self.errorsize_pt, 0)))
4182 topleft2 = graph._addpos(*(topleft+(0, -self.errorsize_pt)))
4183 graph.stroke(path.path(path._moveto(*topleft1),
4184 graph._connect(*(topleft1+topleft)),
4185 graph._connect(*(topleft+topleft2))),
4186 *self.errorbarattrs)
4187 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4188 graph.stroke(path.path(path._moveto(*bottomleft),
4189 graph._connect(*(bottomleft+bottomright)),
4190 graph._connect(*(bottomright+topright)),
4191 graph._connect(*(topright+topleft)),
4192 path.closepath()),
4193 *self.errorbarattrs)
4195 def drawsymbol_pt(self, canvas, x, y, point=None):
4196 canvas.draw(path.path(*self.symbol(self, x, y)), *self.symbolattrs)
4198 def drawsymbol(self, canvas, x, y, point=None):
4199 self.drawsymbol_pt(canvas, unit.topt(x), unit.topt(y), point)
4201 def key(self, c, x, y, width, height):
4202 if self._symbolattrs is not None:
4203 self.drawsymbol_pt(c, x + 0.5 * width, y + 0.5 * height)
4204 if self._lineattrs is not None:
4205 c.stroke(path._line(x, y + 0.5 * height, x + width, y + 0.5 * height), *self.lineattrs)
4207 def drawpoints(self, graph, points):
4208 xaxismin, xaxismax = self.xaxis.getrange()
4209 yaxismin, yaxismax = self.yaxis.getrange()
4210 self.size = unit.length(_getattr(self.size_str), default_type="v")
4211 self.size_pt = unit.topt(self.size)
4212 self.symbol = _getattr(self._symbol)
4213 self.symbolattrs = _getattrs(helper.ensuresequence(self._symbolattrs))
4214 self.errorbarattrs = _getattrs(helper.ensuresequence(self._errorbarattrs))
4215 self.errorsize_pt = self.errorscale * self.size_pt
4216 self.errorsize = self.errorscale * self.size
4217 self.lineattrs = _getattrs(helper.ensuresequence(self._lineattrs))
4218 if self._lineattrs is not None:
4219 clipcanvas = graph.clipcanvas()
4220 lineels = []
4221 haserror = filter(None, (self.xmini, self.ymini, self.xmaxi, self.ymaxi,
4222 self.dxi, self.dyi, self.dxmini, self.dymini, self.dxmaxi, self.dymaxi)) is not None
4223 moveto = 1
4224 for point in points:
4225 drawsymbol = 1
4226 xmin, x, xmax = self.minmidmax(point, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4227 ymin, y, ymax = self.minmidmax(point, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4228 if x is not None and x < xaxismin: drawsymbol = 0
4229 elif x is not None and x > xaxismax: drawsymbol = 0
4230 elif y is not None and y < yaxismin: drawsymbol = 0
4231 elif y is not None and y > yaxismax: drawsymbol = 0
4232 # elif haserror: # TODO: correct clipcanvas handling
4233 # if xmin is not None and xmin < xaxismin: drawsymbol = 0
4234 # elif xmax is not None and xmax < xaxismin: drawsymbol = 0
4235 # elif xmax is not None and xmax > xaxismax: drawsymbol = 0
4236 # elif xmin is not None and xmin > xaxismax: drawsymbol = 0
4237 # elif ymin is not None and ymin < yaxismin: drawsymbol = 0
4238 # elif ymax is not None and ymax < yaxismin: drawsymbol = 0
4239 # elif ymax is not None and ymax > yaxismax: drawsymbol = 0
4240 # elif ymin is not None and ymin > yaxismax: drawsymbol = 0
4241 xpos=ypos=topleft=top=topright=left=center=right=bottomleft=bottom=bottomright=None
4242 if x is not None and y is not None:
4243 try:
4244 center = xpos, ypos = graph.pos_pt(x, y, xaxis=self.xaxis, yaxis=self.yaxis)
4245 except (ValueError, OverflowError): # XXX: exceptions???
4246 pass
4247 if haserror:
4248 if y is not None:
4249 if xmin is not None: left = graph.pos_pt(xmin, y, xaxis=self.xaxis, yaxis=self.yaxis)
4250 if xmax is not None: right = graph.pos_pt(xmax, y, xaxis=self.xaxis, yaxis=self.yaxis)
4251 if x is not None:
4252 if ymax is not None: top = graph.pos_pt(x, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4253 if ymin is not None: bottom = graph.pos_pt(x, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4254 if x is None or y is None:
4255 if ymax is not None:
4256 if xmin is not None: topleft = graph.pos_pt(xmin, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4257 if xmax is not None: topright = graph.pos_pt(xmax, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4258 if ymin is not None:
4259 if xmin is not None: bottomleft = graph.pos_pt(xmin, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4260 if xmax is not None: bottomright = graph.pos_pt(xmax, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4261 if drawsymbol:
4262 if self._errorbarattrs is not None and haserror:
4263 self.drawerrorbar_pt(graph, topleft, top, topright,
4264 left, center, right,
4265 bottomleft, bottom, bottomright, point)
4266 if self._symbolattrs is not None and xpos is not None and ypos is not None:
4267 self.drawsymbol_pt(graph, xpos, ypos, point)
4268 if xpos is not None and ypos is not None:
4269 if moveto:
4270 lineels.append(path._moveto(xpos, ypos))
4271 moveto = 0
4272 else:
4273 lineels.append(path._lineto(xpos, ypos))
4274 else:
4275 moveto = 1
4276 self.path = path.path(*lineels)
4277 if self._lineattrs is not None:
4278 clipcanvas.stroke(self.path, *self.lineattrs)
4281 class changesymbol(changesequence): pass
4284 class _changesymbolcross(changesymbol):
4285 defaultsequence = (symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond)
4288 class _changesymbolplus(changesymbol):
4289 defaultsequence = (symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross)
4292 class _changesymbolsquare(changesymbol):
4293 defaultsequence = (symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus)
4296 class _changesymboltriangle(changesymbol):
4297 defaultsequence = (symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square)
4300 class _changesymbolcircle(changesymbol):
4301 defaultsequence = (symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle)
4304 class _changesymboldiamond(changesymbol):
4305 defaultsequence = (symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle)
4308 class _changesymbolsquaretwice(changesymbol):
4309 defaultsequence = (symbol.square, symbol.square, symbol.triangle, symbol.triangle,
4310 symbol.circle, symbol.circle, symbol.diamond, symbol.diamond)
4313 class _changesymboltriangletwice(changesymbol):
4314 defaultsequence = (symbol.triangle, symbol.triangle, symbol.circle, symbol.circle,
4315 symbol.diamond, symbol.diamond, symbol.square, symbol.square)
4318 class _changesymbolcircletwice(changesymbol):
4319 defaultsequence = (symbol.circle, symbol.circle, symbol.diamond, symbol.diamond,
4320 symbol.square, symbol.square, symbol.triangle, symbol.triangle)
4323 class _changesymboldiamondtwice(changesymbol):
4324 defaultsequence = (symbol.diamond, symbol.diamond, symbol.square, symbol.square,
4325 symbol.triangle, symbol.triangle, symbol.circle, symbol.circle)
4328 changesymbol.cross = _changesymbolcross
4329 changesymbol.plus = _changesymbolplus
4330 changesymbol.square = _changesymbolsquare
4331 changesymbol.triangle = _changesymboltriangle
4332 changesymbol.circle = _changesymbolcircle
4333 changesymbol.diamond = _changesymboldiamond
4334 changesymbol.squaretwice = _changesymbolsquaretwice
4335 changesymbol.triangletwice = _changesymboltriangletwice
4336 changesymbol.circletwice = _changesymbolcircletwice
4337 changesymbol.diamondtwice = _changesymboldiamondtwice
4340 class line(symbol):
4342 def __init__(self, lineattrs=helper.nodefault):
4343 if lineattrs is helper.nodefault:
4344 lineattrs = (changelinestyle(), style.linejoin.round)
4345 symbol.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
4348 class rect(symbol):
4350 def __init__(self, palette=color.palette.Gray):
4351 self.palette = palette
4352 self.colorindex = None
4353 symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
4355 def iterate(self):
4356 raise RuntimeError("style is not iterateable")
4358 def othercolumnkey(self, key, index):
4359 if key == "color":
4360 self.colorindex = index
4361 else:
4362 symbol.othercolumnkey(self, key, index)
4364 def drawerrorbar_pt(self, graph, topleft, top, topright,
4365 left, center, right,
4366 bottomleft, bottom, bottomright, point=None):
4367 color = point[self.colorindex]
4368 if color is not None:
4369 if color != self.lastcolor:
4370 self.rectclipcanvas.set(self.palette.getcolor(color))
4371 if bottom is not None and left is not None:
4372 bottomleft = left[0], bottom[1]
4373 if bottom is not None and right is not None:
4374 bottomright = right[0], bottom[1]
4375 if top is not None and right is not None:
4376 topright = right[0], top[1]
4377 if top is not None and left is not None:
4378 topleft = left[0], top[1]
4379 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4380 self.rectclipcanvas.fill(path.path(path._moveto(*bottomleft),
4381 graph._connect(*(bottomleft+bottomright)),
4382 graph._connect(*(bottomright+topright)),
4383 graph._connect(*(topright+topleft)),
4384 path.closepath()))
4386 def drawpoints(self, graph, points):
4387 if self.colorindex is None:
4388 raise RuntimeError("column 'color' not set")
4389 self.lastcolor = None
4390 self.rectclipcanvas = graph.clipcanvas()
4391 symbol.drawpoints(self, graph, points)
4393 def key(self, c, x, y, width, height):
4394 raise RuntimeError("style doesn't yet provide a key")
4397 class text(symbol):
4399 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=textmodule.halign.center, **args):
4400 self.textindex = None
4401 self.textdx_str = textdx
4402 self.textdy_str = textdy
4403 self._textattrs = textattrs
4404 symbol.__init__(self, **args)
4406 def iteratedict(self):
4407 result = symbol.iteratedict()
4408 result["textattrs"] = _iterateattr(self._textattrs)
4409 return result
4411 def iterate(self):
4412 return textsymbol(**self.iteratedict())
4414 def othercolumnkey(self, key, index):
4415 if key == "text":
4416 self.textindex = index
4417 else:
4418 symbol.othercolumnkey(self, key, index)
4420 def drawsymbol_pt(self, graph, x, y, point=None):
4421 symbol.drawsymbol_pt(self, graph, x, y, point)
4422 if None not in (x, y, point[self.textindex]) and self._textattrs is not None:
4423 graph.text_pt(x + self.textdx_pt, y + self.textdy_pt, str(point[self.textindex]), *helper.ensuresequence(self.textattrs))
4425 def drawpoints(self, graph, points):
4426 self.textdx = unit.length(_getattr(self.textdx_str), default_type="v")
4427 self.textdy = unit.length(_getattr(self.textdy_str), default_type="v")
4428 self.textdx_pt = unit.topt(self.textdx)
4429 self.textdy_pt = unit.topt(self.textdy)
4430 if self._textattrs is not None:
4431 self.textattrs = _getattr(self._textattrs)
4432 if self.textindex is None:
4433 raise RuntimeError("column 'text' not set")
4434 symbol.drawpoints(self, graph, points)
4436 def key(self, c, x, y, width, height):
4437 raise RuntimeError("style doesn't yet provide a key")
4440 class arrow(symbol):
4442 def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
4443 self.linelength_str = linelength
4444 self.arrowsize_str = arrowsize
4445 self.arrowattrs = arrowattrs
4446 self.arrowdict = arrowdict
4447 self.epsilon = epsilon
4448 self.sizeindex = self.angleindex = None
4449 symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
4451 def iterate(self):
4452 raise RuntimeError("style is not iterateable")
4454 def othercolumnkey(self, key, index):
4455 if key == "size":
4456 self.sizeindex = index
4457 elif key == "angle":
4458 self.angleindex = index
4459 else:
4460 symbol.othercolumnkey(self, key, index)
4462 def drawsymbol_pt(self, graph, x, y, point=None):
4463 if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
4464 if point[self.sizeindex] > self.epsilon:
4465 dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
4466 x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
4467 y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
4468 x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
4469 y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
4470 graph.stroke(path.line(x1, y1, x2, y2),
4471 deco.earrow(self.arrowsize*point[self.sizeindex],
4472 **self.arrowdict),
4473 *helper.ensuresequence(self.arrowattrs))
4475 def drawpoints(self, graph, points):
4476 self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
4477 self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
4478 self.arrowsize_pt = unit.topt(self.arrowsize)
4479 self.linelength_pt = unit.topt(self.linelength)
4480 if self.sizeindex is None:
4481 raise RuntimeError("column 'size' not set")
4482 if self.angleindex is None:
4483 raise RuntimeError("column 'angle' not set")
4484 symbol.drawpoints(self, graph, points)
4486 def key(self, c, x, y, width, height):
4487 raise RuntimeError("style doesn't yet provide a key")
4490 class _bariterator(changeattr):
4492 def attr(self, index):
4493 return index, self.counter
4496 class bar:
4498 def __init__(self, fromzero=1, stacked=0, skipmissing=1, xbar=0,
4499 barattrs=helper.nodefault, _usebariterator=helper.nodefault, _previousbar=None):
4500 self.fromzero = fromzero
4501 self.stacked = stacked
4502 self.skipmissing = skipmissing
4503 self.xbar = xbar
4504 if barattrs is helper.nodefault:
4505 self._barattrs = (deco.stroked(color.gray.black), changecolor.Rainbow())
4506 else:
4507 self._barattrs = barattrs
4508 if _usebariterator is helper.nodefault:
4509 self.bariterator = _bariterator()
4510 else:
4511 self.bariterator = _usebariterator
4512 self.previousbar = _previousbar
4514 def iteratedict(self):
4515 result = {}
4516 result["barattrs"] = _iterateattrs(self._barattrs)
4517 return result
4519 def iterate(self):
4520 return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
4521 _usebariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
4523 def setcolumns(self, graph, columns):
4524 def checkpattern(key, index, pattern, iskey, isindex):
4525 if key is not None:
4526 match = pattern.match(key)
4527 if match:
4528 if isindex is not None: raise ValueError("multiple key specification")
4529 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4530 key = None
4531 iskey = match.groups()[0]
4532 isindex = index
4533 return key, iskey, isindex
4535 xkey = ykey = None
4536 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4537 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4538 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4539 xi = yi = None
4540 for key, index in columns.items():
4541 key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
4542 key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
4543 if key is not None:
4544 self.othercolumnkey(key, index)
4545 if None in (xkey, ykey): raise ValueError("incomplete axis specification")
4546 if self.xbar:
4547 self.nkey, self.ni = ykey, yi
4548 self.vkey, self.vi = xkey, xi
4549 else:
4550 self.nkey, self.ni = xkey, xi
4551 self.vkey, self.vi = ykey, yi
4552 self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
4554 def getranges(self, points):
4555 index, count = _getattr(self.bariterator)
4556 if count != 1 and self.stacked != 1:
4557 if self.stacked > 1:
4558 index = divmod(index, self.stacked)[0]
4560 vmin = vmax = None
4561 for point in points:
4562 if not self.skipmissing:
4563 if count != 1 and self.stacked != 1:
4564 self.naxis.setname(point[self.ni], index)
4565 else:
4566 self.naxis.setname(point[self.ni])
4567 try:
4568 v = point[self.vi] + 0.0
4569 if vmin is None or v < vmin: vmin = v
4570 if vmax is None or v > vmax: vmax = v
4571 except (TypeError, ValueError):
4572 pass
4573 else:
4574 if self.skipmissing:
4575 if count != 1 and self.stacked != 1:
4576 self.naxis.setname(point[self.ni], index)
4577 else:
4578 self.naxis.setname(point[self.ni])
4579 if self.fromzero:
4580 if vmin > 0: vmin = 0
4581 if vmax < 0: vmax = 0
4582 return {self.vkey: (vmin, vmax)}
4584 def drawpoints(self, graph, points):
4585 index, count = _getattr(self.bariterator)
4586 dostacked = (self.stacked != 0 and
4587 (self.stacked == 1 or divmod(index, self.stacked)[1]) and
4588 (self.stacked != 1 or index))
4589 if self.stacked > 1:
4590 index = divmod(index, self.stacked)[0]
4591 vmin, vmax = self.vaxis.getrange()
4592 self.barattrs = _getattrs(helper.ensuresequence(self._barattrs))
4593 if self.stacked:
4594 self.stackedvalue = {}
4595 for point in points:
4596 try:
4597 n = point[self.ni]
4598 v = point[self.vi]
4599 if self.stacked:
4600 self.stackedvalue[n] = v
4601 if count != 1 and self.stacked != 1:
4602 minid = (n, index, 0)
4603 maxid = (n, index, 1)
4604 else:
4605 minid = (n, 0)
4606 maxid = (n, 1)
4607 if self.xbar:
4608 x1pos, y1pos = graph.pos_pt(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
4609 x2pos, y2pos = graph.pos_pt(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4610 else:
4611 x1pos, y1pos = graph.pos_pt(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
4612 x2pos, y2pos = graph.pos_pt(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
4613 if dostacked:
4614 if self.xbar:
4615 x3pos, y3pos = graph.pos_pt(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
4616 x4pos, y4pos = graph.pos_pt(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
4617 else:
4618 x3pos, y3pos = graph.pos_pt(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4619 x4pos, y4pos = graph.pos_pt(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4620 else:
4621 if self.fromzero:
4622 if self.xbar:
4623 x3pos, y3pos = graph.pos_pt(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4624 x4pos, y4pos = graph.pos_pt(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
4625 else:
4626 x3pos, y3pos = graph.pos_pt(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4627 x4pos, y4pos = graph.pos_pt(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4628 else:
4629 #x3pos, y3pos = graph.tickpoint_pt(maxid, axis=self.naxis)
4630 #x4pos, y4pos = graph.tickpoint_pt(minid, axis=self.naxis)
4631 x3pos, y3pos = graph.axespos[self.nkey].tickpoint_pt(maxid)
4632 x4pos, y4pos = graph.axespos[self.nkey].tickpoint_pt(minid)
4633 if self.barattrs is not None:
4634 graph.fill(path.path(path._moveto(x1pos, y1pos),
4635 graph._connect(x1pos, y1pos, x2pos, y2pos),
4636 graph._connect(x2pos, y2pos, x3pos, y3pos),
4637 graph._connect(x3pos, y3pos, x4pos, y4pos),
4638 graph._connect(x4pos, y4pos, x1pos, y1pos), # no closepath (might not be straight)
4639 path.closepath()), *self.barattrs)
4640 except (TypeError, ValueError): pass
4642 def key(self, c, x, y, width, height):
4643 c.fill(path._rect(x, y, width, height), *self.barattrs)
4646 #class surface:
4648 # def setcolumns(self, graph, columns):
4649 # self.columns = columns
4651 # def getranges(self, points):
4652 # return {"x": (0, 10), "y": (0, 10), "z": (0, 1)}
4654 # def drawpoints(self, graph, points):
4655 # pass
4659 ################################################################################
4660 # data
4661 ################################################################################
4664 class data:
4666 defaultstyle = symbol
4668 def __init__(self, file, title=helper.nodefault, context={}, **columns):
4669 self.title = title
4670 if helper.isstring(file):
4671 self.data = datamodule.datafile(file)
4672 else:
4673 self.data = file
4674 if title is helper.nodefault:
4675 self.title = "(unknown)"
4676 else:
4677 self.title = title
4678 self.columns = {}
4679 for key, column in columns.items():
4680 try:
4681 self.columns[key] = self.data.getcolumnno(column)
4682 except datamodule.ColumnError:
4683 self.columns[key] = len(self.data.titles)
4684 self.data.addcolumn(column, context=context)
4686 def setstyle(self, graph, style):
4687 self.style = style
4688 self.style.setcolumns(graph, self.columns)
4690 def getranges(self):
4691 return self.style.getranges(self.data.data)
4693 def setranges(self, ranges):
4694 pass
4696 def draw(self, graph):
4697 self.style.drawpoints(graph, self.data.data)
4700 class function:
4702 defaultstyle = line
4704 def __init__(self, expression, title=helper.nodefault, min=None, max=None, points=100, parser=mathtree.parser(), context={}):
4705 if title is helper.nodefault:
4706 self.title = expression
4707 else:
4708 self.title = title
4709 self.min = min
4710 self.max = max
4711 self.points = points
4712 self.context = context
4713 self.result, expression = [x.strip() for x in expression.split("=")]
4714 self.mathtree = parser.parse(expression)
4715 self.variable = None
4716 self.evalranges = 0
4718 def setstyle(self, graph, style):
4719 for variable in self.mathtree.VarList():
4720 if variable in graph.axes.keys():
4721 if self.variable is None:
4722 self.variable = variable
4723 else:
4724 raise ValueError("multiple variables found")
4725 if self.variable is None:
4726 raise ValueError("no variable found")
4727 self.xaxis = graph.axes[self.variable]
4728 self.style = style
4729 self.style.setcolumns(graph, {self.variable: 0, self.result: 1})
4731 def getranges(self):
4732 if self.evalranges:
4733 return self.style.getranges(self.data)
4734 if None not in (self.min, self.max):
4735 return {self.variable: (self.min, self.max)}
4737 def setranges(self, ranges):
4738 if ranges.has_key(self.variable):
4739 min, max = ranges[self.variable]
4740 if self.min is not None: min = self.min
4741 if self.max is not None: max = self.max
4742 vmin = self.xaxis.convert(min)
4743 vmax = self.xaxis.convert(max)
4744 self.data = []
4745 for i in range(self.points):
4746 self.context[self.variable] = x = self.xaxis.invert(vmin + (vmax-vmin)*i / (self.points-1.0))
4747 try:
4748 y = self.mathtree.Calc(**self.context)
4749 except (ArithmeticError, ValueError):
4750 y = None
4751 self.data.append((x, y))
4752 self.evalranges = 1
4754 def draw(self, graph):
4755 self.style.drawpoints(graph, self.data)
4758 class paramfunction:
4760 defaultstyle = line
4762 def __init__(self, varname, min, max, expression, title=helper.nodefault, points=100, parser=mathtree.parser(), context={}):
4763 if title is helper.nodefault:
4764 self.title = expression
4765 else:
4766 self.title = title
4767 self.varname = varname
4768 self.min = min
4769 self.max = max
4770 self.points = points
4771 self.expression = {}
4772 self.mathtrees = {}
4773 varlist, expressionlist = expression.split("=")
4774 parsestr = mathtree.ParseStr(expressionlist)
4775 for key in varlist.split(","):
4776 key = key.strip()
4777 if self.mathtrees.has_key(key):
4778 raise ValueError("multiple assignment in tuple")
4779 try:
4780 self.mathtrees[key] = parser.ParseMathTree(parsestr)
4781 break
4782 except mathtree.CommaFoundMathTreeParseError, e:
4783 self.mathtrees[key] = e.MathTree
4784 else:
4785 raise ValueError("unpack tuple of wrong size")
4786 if len(varlist.split(",")) != len(self.mathtrees.keys()):
4787 raise ValueError("unpack tuple of wrong size")
4788 self.data = []
4789 for i in range(self.points):
4790 context[self.varname] = self.min + (self.max-self.min)*i / (self.points-1.0)
4791 line = []
4792 for key, tree in self.mathtrees.items():
4793 line.append(tree.Calc(**context))
4794 self.data.append(line)
4796 def setstyle(self, graph, style):
4797 self.style = style
4798 columns = {}
4799 for key, index in zip(self.mathtrees.keys(), xrange(sys.maxint)):
4800 columns[key] = index
4801 self.style.setcolumns(graph, columns)
4803 def getranges(self):
4804 return self.style.getranges(self.data)
4806 def setranges(self, ranges):
4807 pass
4809 def draw(self, graph):
4810 self.style.drawpoints(graph, self.data)