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