more axispos work; all test graphs in the functional testsuite are running again
[PyX.git] / pyx / graph.py
blob2cec7954dc04ca6fe4dfec0cb4a927a2d864cdbc
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 import re, math, string, sys
26 import bbox, box, canvas, color, deco, helper, path, style, unit, mathtree
27 import text as textmodule
28 import data as datamodule
29 import trafo as trafomodule
32 goldenmean = 0.5 * (math.sqrt(5) + 1)
35 ################################################################################
36 # maps
37 ################################################################################
39 class _Imap:
40 """interface definition of a map
41 maps convert a value into another value by bijective transformation f"""
43 def convert(self, x):
44 "returns f(x)"
46 def invert(self, y):
47 "returns f^-1(y) where f^-1 is the inverse transformation (x=f^-1(f(x)) for all x)"
49 def setbasepoints(self, basepoints):
50 """set basepoints for the convertions
51 basepoints are tuples (x, y) with y == f(x) and x == f^-1(y)
52 the number of basepoints needed might depend on the transformation
53 usually two pairs are needed like for linear maps, logarithmic maps, etc."""
56 class _linmap:
57 "linear mapping"
59 __implements__ = _Imap
61 def setbasepoints(self, basepoints):
62 self.dydx = (basepoints[1][1] - basepoints[0][1]) / float(basepoints[1][0] - basepoints[0][0])
63 self.dxdy = (basepoints[1][0] - basepoints[0][0]) / float(basepoints[1][1] - basepoints[0][1])
64 self.x1 = basepoints[0][0]
65 self.y1 = basepoints[0][1]
67 def convert(self, value):
68 return self.y1 + self.dydx * (value - self.x1)
70 def invert(self, value):
71 return self.x1 + self.dxdy * (value - self.y1)
74 class _logmap:
75 "logarithmic mapping"
76 __implements__ = _Imap
78 def setbasepoints(self, basepoints):
79 self.dydx = ((basepoints[1][1] - basepoints[0][1]) /
80 float(math.log(basepoints[1][0]) - math.log(basepoints[0][0])))
81 self.dxdy = ((math.log(basepoints[1][0]) - math.log(basepoints[0][0])) /
82 float(basepoints[1][1] - basepoints[0][1]))
83 self.x1 = math.log(basepoints[0][0])
84 self.y1 = basepoints[0][1]
85 return self
87 def convert(self, value):
88 return self.y1 + self.dydx * (math.log(value) - self.x1)
90 def invert(self, value):
91 return math.exp(self.x1 + self.dxdy * (value - self.y1))
95 ################################################################################
96 # partitioner
97 # please note the nomenclature:
98 # - a partition is a list of tick instances; to reduce name clashes, a
99 # partition is called ticks
100 # - a partitioner is a class creating a single or several ticks
101 # - an axis has a part attribute where it stores a partitioner or/and some
102 # (manually set) ticks -> the part attribute is used to create the ticks
103 # in the axis finish method
104 ################################################################################
107 class frac:
108 """fraction class for rational arithmetics
109 the axis partitioning uses rational arithmetics (with infinite accuracy)
110 basically it contains self.enum and self.denom"""
112 def stringfrac(self, s):
113 "converts a string 0.123 into a frac"
114 expparts = s.split("e")
115 if len(expparts) > 2:
116 raise ValueError("multiple 'e' found in '%s'" % s)
117 commaparts = expparts[0].split(".")
118 if len(commaparts) > 2:
119 raise ValueError("multiple '.' found in '%s'" % expparts[0])
120 if len(commaparts) == 1:
121 commaparts = [commaparts[0], ""]
122 result = frac((1, 10l), power=len(commaparts[1]))
123 neg = len(commaparts[0]) and commaparts[0][0] == "-"
124 if neg:
125 commaparts[0] = commaparts[0][1:]
126 elif len(commaparts[0]) and commaparts[0][0] == "+":
127 commaparts[0] = commaparts[0][1:]
128 if len(commaparts[0]):
129 if not commaparts[0].isdigit():
130 raise ValueError("unrecognized characters in '%s'" % s)
131 x = long(commaparts[0])
132 else:
133 x = 0
134 if len(commaparts[1]):
135 if not commaparts[1].isdigit():
136 raise ValueError("unrecognized characters in '%s'" % s)
137 y = long(commaparts[1])
138 else:
139 y = 0
140 result.enum = x*result.denom+y
141 if neg:
142 result.enum = -result.enum
143 if len(expparts) == 2:
144 neg = expparts[1][0] == "-"
145 if neg:
146 expparts[1] = expparts[1][1:]
147 elif expparts[1][0] == "+":
148 expparts[1] = expparts[1][1:]
149 if not expparts[1].isdigit():
150 raise ValueError("unrecognized characters in '%s'" % s)
151 if neg:
152 result *= frac((1, 10l), power=long(expparts[1]))
153 else:
154 result *= frac((10, 1l), power=long(expparts[1]))
155 return result
157 def floatfrac(self, x, floatprecision):
158 "converts a float into a frac with finite resolution"
159 if helper.isinteger(floatprecision) and floatprecision < 0:
160 # this would be extremly vulnerable
161 raise RuntimeError("float resolution must be non-negative integer")
162 return self.stringfrac(("%%.%ig" % floatprecision) % x)
164 def __init__(self, x, power=None, floatprecision=10):
165 "for power!=None: frac=(enum/denom)**power"
166 if helper.isnumber(x):
167 value = self.floatfrac(x, floatprecision)
168 enum, denom = value.enum, value.denom
169 elif helper.isstring(x):
170 fraction = x.split("/")
171 if len(fraction) > 2:
172 raise ValueError("multiple '/' found in '%s'" % x)
173 value = self.stringfrac(fraction[0])
174 if len(fraction) == 2:
175 value2 = self.stringfrac(fraction[1])
176 value = value / value2
177 enum, denom = value.enum, value.denom
178 else:
179 try:
180 enum, denom = x
181 except (TypeError, AttributeError):
182 enum, denom = x.enum, x.denom
183 if not helper.isinteger(enum) or not helper.isinteger(denom): raise TypeError("integer type expected")
184 if not denom: raise ZeroDivisionError("zero denominator")
185 if power != None:
186 if not helper.isinteger(power): raise TypeError("integer type expected")
187 if power >= 0:
188 self.enum = long(enum) ** power
189 self.denom = long(denom) ** power
190 else:
191 self.enum = long(denom) ** (-power)
192 self.denom = long(enum) ** (-power)
193 else:
194 self.enum = enum
195 self.denom = denom
197 def __cmp__(self, other):
198 if other is None:
199 return 1
200 return cmp(self.enum * other.denom, other.enum * self.denom)
202 def __abs__(self):
203 return frac((abs(self.enum), abs(self.denom)))
205 def __mul__(self, other):
206 return frac((self.enum * other.enum, self.denom * other.denom))
208 def __div__(self, other):
209 return frac((self.enum * other.denom, self.denom * other.enum))
211 def __float__(self):
212 "caution: avoid final precision of floats"
213 return float(self.enum) / self.denom
215 def __str__(self):
216 return "%i/%i" % (self.enum, self.denom)
219 class tick(frac):
220 """tick class
221 a tick is a frac enhanced by
222 - self.ticklevel (0 = tick, 1 = subtick, etc.)
223 - self.labellevel (0 = label, 1 = sublabel, etc.)
224 - self.label (a string) and self.labelattrs (a list, defaults to [])
225 When ticklevel or labellevel is None, no tick or label is present at that value.
226 When label is None, it should be automatically created (and stored), once the
227 an axis painter needs it. Classes, which implement _Itexter do precisely that."""
229 def __init__(self, pos, ticklevel=0, labellevel=0, label=None, labelattrs=[], **kwargs):
230 """initializes the instance
231 - see class description for the parameter description
232 - **kwargs are passed to the frac constructor"""
233 frac.__init__(self, pos, **kwargs)
234 self.ticklevel = ticklevel
235 self.labellevel = labellevel
236 self.label = label
237 self.labelattrs = helper.ensurelist(labelattrs)[:]
239 def merge(self, other):
240 """merges two ticks together:
241 - the lower ticklevel/labellevel wins
242 - the label is *never* taken over from other
243 - the ticks should be at the same position (otherwise it doesn't make sense)
244 -> this is NOT checked"""
245 if self.ticklevel is None or (other.ticklevel is not None and other.ticklevel < self.ticklevel):
246 self.ticklevel = other.ticklevel
247 if self.labellevel is None or (other.labellevel is not None and other.labellevel < self.labellevel):
248 self.labellevel = other.labellevel
251 def _mergeticklists(list1, list2):
252 """helper function to merge tick lists
253 - return a merged list of ticks out of list1 and list2
254 - CAUTION: original lists have to be ordered
255 (the returned list is also ordered)
256 - CAUTION: original lists are modified and they share references to
257 the result list!"""
258 # TODO: improve this using bisect?!
259 if list1 is None: return list2
260 if list2 is None: return 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 _Ipart:
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 linpart:
343 """linear partition scheme
344 ticks and label distances are explicitly provided to the constructor"""
346 __implements__ = _Ipart
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 autolinpart:
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__ = _Ipart
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 = linpart(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 = linpart(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 = linpart(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 logpart(linpart):
513 """logarithmic partition scheme
514 ticks and label positions are explicitly provided to the constructor"""
516 __implements__ = _Ipart
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 autologpart(logpart):
615 """automatic logarithmic partition scheme
616 possible tick positions are explicitly provided to the constructor"""
618 __implements__ = _Ipart
620 defaultvariants = (((logpart.pre1exp, # ticks
621 logpart.pre1to9exp), # subticks
622 (logpart.pre1exp, # labels
623 logpart.pre125exp)), # sublevels
625 ((logpart.pre1exp, # ticks
626 logpart.pre1to9exp), # subticks
627 None), # labels like ticks
629 ((logpart.pre1exp2, # ticks
630 logpart.pre1exp), # subticks
631 None), # labels like ticks
633 ((logpart.pre1exp3, # ticks
634 logpart.pre1exp), # subticks
635 None), # labels like ticks
637 ((logpart.pre1exp4, # ticks
638 logpart.pre1exp), # subticks
639 None), # labels like ticks
641 ((logpart.pre1exp5, # ticks
642 logpart.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 = logpart(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 = logpart(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 = logpart(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 >= maxticklevel:
858 maxticklevel = tick.ticklevel + 1
859 if 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(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 nomantissaexp=r"{10^{%s}}",
1193 minusnomantissaexp=r"{-10^{%s}}",
1194 mantissamin=frac((1, 1)), mantissamax=frac((10, 1)),
1195 skipmantissa1=0, skipallmantissa1=1,
1196 mantissatexter=decimaltexter()):
1197 r"""initializes the instance
1198 - plus or minus (string) is inserted for non-negative or negative exponents
1199 - mantissaexp (string) is taken as a format string generating the exponent;
1200 it has to contain exactly two string insert operators "%s" --
1201 the first for the mantissa and the second for the exponent;
1202 examples are r"{{%s}\cdot10^{%s}}" and r"{{%s}{\rm e}^{%s}}"
1203 - nomantissaexp (string) is taken as a format string generating the exponent
1204 when the mantissa is one and should be skipped; it has to contain
1205 exactly one string insert operators "%s" for the exponent;
1206 an examples is r"{10^{%s}}"
1207 - minusnomantissaexp (string) is taken as a format string generating the exponent
1208 when the mantissa is minus one and should be skipped; it has to contain
1209 exactly one string insert operators "%s" for the exponent; might be set to None
1210 to disallow skipping of any mantissa minus one
1211 an examples is r"{-10^{%s}}"
1212 - mantissamin and mantissamax are the minimum and maximum of the mantissa;
1213 they are frac instances greater than zero and mantissamin < mantissamax;
1214 the sign of the tick is ignored here
1215 - skipmantissa1 (boolean) turns on skipping of any mantissa equals one
1216 (and minus when minusnomantissaexp is set)
1217 - skipallmantissa1 (boolean) as above, but all mantissas must be 1
1218 - mantissatexter is the texter for the mantissa"""
1219 self.plus = plus
1220 self.minus = minus
1221 self.mantissaexp = mantissaexp
1222 self.nomantissaexp = nomantissaexp
1223 self.minusnomantissaexp = minusnomantissaexp
1224 self.mantissamin = mantissamin
1225 self.mantissamax = mantissamax
1226 self.mantissamindivmax = self.mantissamin / self.mantissamax
1227 self.mantissamaxdivmin = self.mantissamax / self.mantissamin
1228 self.skipmantissa1 = skipmantissa1
1229 self.skipallmantissa1 = skipallmantissa1
1230 self.mantissatexter = mantissatexter
1232 def labels(self, ticks):
1233 labeledticks = []
1234 for tick in ticks:
1235 if tick.label is None and tick.labellevel is not None:
1236 tick.temp_orgenum, tick.temp_orgdenom = tick.enum, tick.denom
1237 labeledticks.append(tick)
1238 tick.temp_exp = 0
1239 if tick.enum:
1240 while abs(tick) >= self.mantissamax:
1241 tick.temp_exp += 1
1242 x = tick * self.mantissamindivmax
1243 tick.enum, tick.denom = x.enum, x.denom
1244 while abs(tick) < self.mantissamin:
1245 tick.temp_exp -= 1
1246 x = tick * self.mantissamaxdivmin
1247 tick.enum, tick.denom = x.enum, x.denom
1248 if tick.temp_exp < 0:
1249 tick.temp_exp = "%s%i" % (self.minus, -tick.temp_exp)
1250 else:
1251 tick.temp_exp = "%s%i" % (self.plus, tick.temp_exp)
1252 self.mantissatexter.labels(labeledticks)
1253 if self.minusnomantissaexp is not None:
1254 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if abs(tick.enum) == abs(tick.denom)])
1255 else:
1256 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if tick.enum == tick.denom])
1257 for tick in labeledticks:
1258 if (self.skipallmantissa1 and allmantissa1 or
1259 (self.skipmantissa1 and (tick.enum == tick.denom or
1260 (tick.enum == -tick.denom and self.minusnomantissaexp is not None)))):
1261 if tick.enum == tick.denom:
1262 tick.label = self.nomantissaexp % tick.temp_exp
1263 else:
1264 tick.label = self.minusnomantissaexp % tick.temp_exp
1265 else:
1266 tick.label = self.mantissaexp % (tick.label, tick.temp_exp)
1267 tick.enum, tick.denom = tick.temp_orgenum, tick.temp_orgdenom
1269 # del tick.temp_orgenum # we've inserted those temporary variables ... and do not care any longer about them
1270 # del tick.temp_orgdenom
1271 # del tick.temp_exp
1274 class defaulttexter:
1275 "a texter creating decimal or exponential labels"
1277 __implements__ = _Itexter
1279 def __init__(self, smallestdecimal=frac((1, 1000)),
1280 biggestdecimal=frac((9999, 1)),
1281 equaldecision=1,
1282 decimaltexter=decimaltexter(),
1283 exponentialtexter=exponentialtexter()):
1284 r"""initializes the instance
1285 - smallestdecimal and biggestdecimal are the smallest and
1286 biggest decimal values, where the decimaltexter should be used;
1287 they are frac instances; the sign of the tick is ignored here;
1288 a tick at zero is considered for the decimaltexter as well
1289 - equaldecision (boolean) uses decimaltexter or exponentialtexter
1290 globaly (set) or for each tick separately (unset)
1291 - decimaltexter and exponentialtexter are texters to be used"""
1292 self.smallestdecimal = smallestdecimal
1293 self.biggestdecimal = biggestdecimal
1294 self.equaldecision = equaldecision
1295 self.decimaltexter = decimaltexter
1296 self.exponentialtexter = exponentialtexter
1298 def labels(self, ticks):
1299 decticks = []
1300 expticks = []
1301 for tick in ticks:
1302 if tick.label is None and tick.labellevel is not None:
1303 if not tick.enum or (abs(tick) >= self.smallestdecimal and abs(tick) <= self.biggestdecimal):
1304 decticks.append(tick)
1305 else:
1306 expticks.append(tick)
1307 if self.equaldecision:
1308 if len(expticks):
1309 self.exponentialtexter.labels(ticks)
1310 else:
1311 self.decimaltexter.labels(ticks)
1312 else:
1313 for tick in decticks:
1314 self.decimaltexter.labels([tick])
1315 for tick in expticks:
1316 self.exponentialtexter.labels([tick])
1319 ################################################################################
1320 # axis painter
1321 ################################################################################
1324 class axiscanvas(canvas._canvas):
1325 """axis canvas
1326 - an axis canvas is a regular canvas returned by an
1327 axispainters painter method
1328 - it contains a PyX length extent to be used for the
1329 alignment of additional axes; the axis extent should
1330 be handled by the axispainters painter method; you may
1331 apprehend this as a size information comparable to a
1332 bounding box, which must be handled manually
1333 - it contains a list of textboxes called labels which are
1334 used to rate the distances between the labels if needed
1335 by the axis later on; the painter method has not only to
1336 insert the labels into this canvas, but should also fill
1337 this list, when a rating of the distances should be
1338 performed by the axis"""
1340 # __implements__ = sole implementation
1342 def __init__(self, *args, **kwargs):
1343 """initializes the instance
1344 - sets extent to zero
1345 - sets labels to an empty list"""
1346 canvas._canvas.__init__(self, *args, **kwargs)
1347 self.extent = 0
1348 self.labels = []
1351 class rotatetext:
1352 """create rotations accordingly to tick directions
1353 - upsidedown rotations are suppressed by rotating them by another 180 degree"""
1355 # __implements__ = sole implementation
1357 def __init__(self, direction, epsilon=1e-10):
1358 """initializes the instance
1359 - direction is an angle to be used relative to the tick direction
1360 - epsilon is the value by which 90 degrees can be exceeded before
1361 an 180 degree rotation is added"""
1362 self.direction = direction
1363 self.epsilon = epsilon
1365 def trafo(self, dx, dy):
1366 """returns a rotation transformation accordingly to the tick direction
1367 - dx and dy are the direction of the tick"""
1368 direction = self.direction - math.atan2(dy, dx) * 180 / math.pi
1369 while (direction > 90 + self.epsilon):
1370 direction -= 180
1371 while (direction < -90 - self.epsilon):
1372 direction += 180
1373 return trafomodule.rotate(direction)
1376 rotatetext.parallel = rotatetext(90)
1377 rotatetext.orthogonal = rotatetext(180)
1380 class _Iaxispainter:
1381 "class for painting axes"
1383 def paint(self, axispos, axis, ac=None):
1384 """paint the axis into an axiscanvas
1385 - returns the axiscanvas
1386 - when no axiscanvas is provided (the typical case), a new
1387 axiscanvas is created. however, when extending an painter
1388 by inheritance, painting on the same axiscanvas is supported
1389 by setting the axiscanvas attribute
1390 - axispos is an instance, which implements _Iaxispos to
1391 define the tick positions
1392 - the axis and should not be modified (we may
1393 add some temporary variables like axis.ticks[i].temp_xxx,
1394 which might be used just temporary) -- the idea is that
1395 all things can be used several times
1396 - also do not modify the instance (self) -- even this
1397 instance might be used several times; thus do not modify
1398 attributes like self.titleattrs etc. (use local copies)
1399 - the method might access some additional attributes from
1400 the axis, e.g. the axis title -- the axis painter should
1401 document this behavior and rely on the availability of
1402 those attributes -> it becomes a question of the proper
1403 usage of the combination of axis & axispainter
1404 - the axiscanvas is a axiscanvas instance and should be
1405 filled with ticks, labels, title, etc.; note that the
1406 extent and labels instance variables should be handled
1407 as documented in the axiscanvas"""
1410 class _Iaxispos:
1411 """interface definition of axis tick position methods
1412 - these methods are used for the postitioning of the ticks
1413 when painting an axis"""
1414 # TODO: should we add a local transformation (for label text etc?)
1415 # (this might replace tickdirection (and even tickposition?))
1417 def basepath(self, x1=None, x2=None):
1418 """return the basepath as a path
1419 - x1 is the start position; if not set, the basepath starts
1420 from the beginning of the axis, which might imply a
1421 value outside of the graph coordinate range [0; 1]
1422 - x2 is analogous to x1, but for the end position"""
1424 def vbasepath(self, v1=None, v2=None):
1425 """return the basepath as a path
1426 - like basepath, but for graph coordinates"""
1428 def gridpath(self, x):
1429 """return the gridpath as a path for a given position x
1430 - might return None when no gridpath is available"""
1432 def vgridpath(self, v):
1433 """return the gridpath as a path for a given position v
1434 in graph coordinates
1435 - might return None when no gridpath is available"""
1437 def tickpoint_pt(self, x):
1438 """return the position at the basepath as a tuple (x, y) in
1439 postscript points for the position x"""
1441 def tickpoint(self, x):
1442 """return the position at the basepath as a tuple (x, y) in
1443 in PyX length for the position x"""
1445 def vtickpoint_pt(self, v):
1446 "like tickpoint_pt, but for graph coordinates"
1448 def vtickpoint(self, v):
1449 "like tickpoint, but for graph coordinates"
1451 def tickdirection(self, x):
1452 """return the direction of a tick as a tuple (dx, dy) for the
1453 position x (the direction points towards the graph)"""
1455 def vtickdirection(self, v):
1456 """like tickposition, but for graph coordinates"""
1459 class _axispos:
1460 """implements those parts of _Iaxispos which can be build
1461 out of the axis convert method and other _Iaxispos methods
1462 - base _Iaxispos methods, which need to be implemented:
1463 - vbasepath
1464 - vgridpath
1465 - vtickpoint_pt
1466 - vtickdirection
1467 - other methods needed for _Iaxispos are build out of those
1468 listed above when this class is inherited"""
1470 def __init__(self, convert):
1471 """initializes the instance
1472 - convert is a convert method from an axis"""
1473 self.convert = convert
1475 def basepath(self, x1=None, x2=None):
1476 if x1 is None:
1477 if x2 is None:
1478 return self.vbasepath()
1479 else:
1480 return self.vbasepath(v2=self.convert(x2))
1481 else:
1482 if x2 is None:
1483 return self.vbasepath(v1=self.convert(x1))
1484 else:
1485 return self.vbasepath(v1=self.convert(x1), v2=self.convert(x2))
1487 def gridpath(self, x):
1488 return self.vgridpath(self.convert(x))
1490 def tickpoint_pt(self, x):
1491 return self.vtickpoint_pt(self.convert(x))
1493 def tickpoint(self, x):
1494 return self.vtickpoint(self.convert(x))
1496 def vtickpoint(self, v):
1497 return [unit.t_pt(x) for x in self.vtickpoint(v)]
1499 def tickdirection(self, x):
1500 return self.vtickdirection(self.convert(x))
1503 class pathaxispos(_axispos):
1504 """axis tick position methods along an arbitrary path"""
1506 __implements__ = _Iaxispos
1508 def __init__(self, p, convert, direction=1):
1509 self.path = p
1510 self.normpath = path.normpath(p)
1511 self.arclength = self.normpath.arclength(p)
1512 _axispos.__init__(self, convert)
1513 self.direction = direction
1515 def vbasepath(self, v1=None, v2=None):
1516 if v1 is None:
1517 if v2 is None:
1518 return self.path
1519 else:
1520 return self.normpath.split(self.normpath.lentopar(v2 * self.arclength))[0]
1521 else:
1522 if v2 is None:
1523 return self.normpath.split(self.normpath.lentopar(v1 * self.arclength))[1]
1524 else:
1525 return self.normpath.split(*self.normpath.lentopar([v1 * self.arclength, v2 * self.arclength]))[1]
1527 def vgridpath(self, v):
1528 return None
1530 def tickpoint_pt(self, v):
1531 # XXX: path._at missing!
1532 return [unit.topt(x) for x in self.normpath.at(self.normpath.lentopar(v * self.arclength))]
1534 def vtickdirection(self, v):
1535 t = self.normpath.tangent(self.normpath.lentopar(v * self.arclength))
1536 # XXX: path._begin and path._end missing!
1537 tbegin = [unit.topt[x] for x in t.begin()]
1538 tend = [unit.topt[x] for x in t.end()]
1539 dx = tend[0]-tbegin[0]
1540 dy = tend[1]-tbegin[1]
1541 norm = math.sqrt(dx*dx + dy*dy)
1542 if self.direction == 1:
1543 return dy/norm, -dx/norm
1544 elif self.direction == -1:
1545 return -dy/norm, dx/norm
1546 raise RuntimeError("unknown direction")
1549 class axistitlepainter:
1550 """class for painting an axis title
1551 - the axis must have a title attribute when using this painter;
1552 this title might be None"""
1554 __implements__ = _Iaxispainter
1556 def __init__(self, titledist="0.3 cm",
1557 titleattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1558 titledirection=rotatetext.parallel,
1559 titlepos=0.5,
1560 texrunner=textmodule.defaulttexrunner):
1561 """initialized the instance
1562 - titledist is a visual PyX length giving the distance
1563 of the title from the axis extent already there (a title might
1564 be added after labels or other things are plotted already)
1565 - labelattrs is a list of attributes for a texrunners text
1566 method; a single is allowed without being a list; None
1567 turns off the title
1568 - titledirection is an instance of rotatetext or None
1569 - titlepos is the position of the title in graph coordinates
1570 - texrunner is the texrunner to be used to create text
1571 (the texrunner is available for further use in derived
1572 classes as instance variable texrunner)"""
1573 self.titledist_str = titledist
1574 self.titleattrs = titleattrs
1575 self.titledirection = titledirection
1576 self.titlepos = titlepos
1577 self.texrunner = texrunner
1579 def paint(self, axispos, axis, ac=None):
1580 if ac is None:
1581 ac = axiscanvas()
1582 if axis.title is not None and self.titleattrs is not None:
1583 titledist = unit.length(self.titledist_str, default_type="v")
1584 x, y = axispos.vtickpoint_pt(self.titlepos)
1585 dx, dy = axispos.vtickdirection(self.titlepos)
1586 titleattrs = helper.ensurelist(self.titleattrs)
1587 if self.titledirection is not None:
1588 titleattrs.append(self.titledirection.trafo(dx, dy))
1589 title = self.texrunner.text_pt(x, y, axis.title, *titleattrs)
1590 ac.extent += titledist
1591 title.linealign(ac.extent, -dx, -dy)
1592 ac.extent += title.extent(dx, dy)
1593 ac.insert(title)
1594 return ac
1597 class axispainter(axistitlepainter):
1598 """class for painting the ticks and labels of an axis
1599 - the inherited titleaxispainter is used to paint the title of
1600 the axis
1601 - note that the type of the elements of ticks given as an argument
1602 of the paint method must be suitable for the tick position methods
1603 of the axis"""
1605 __implements__ = _Iaxispainter
1607 defaultticklengths = ["%0.5f cm" % (0.2*goldenmean**(-i)) for i in range(10)]
1609 def __init__(self, innerticklengths=defaultticklengths,
1610 outerticklengths=None,
1611 tickattrs=(),
1612 gridattrs=None,
1613 zeropathattrs=(),
1614 basepathattrs=style.linecap.square,
1615 labeldist="0.3 cm",
1616 labelattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1617 labeldirection=None,
1618 labelhequalize=0,
1619 labelvequalize=1,
1620 **kwargs):
1621 """initializes the instance
1622 - innerticklenths and outerticklengths are two lists of
1623 visual PyX lengths for ticks, subticks, etc. plotted inside
1624 and outside of the graph; when a single value is given, it
1625 is used for all tick levels; None turns off ticks inside or
1626 outside of the graph
1627 - tickattrs are a list of stroke attributes for the ticks;
1628 a single entry is allowed without being a list; None turns
1629 off ticks
1630 - gridattrs are a list of lists used as stroke
1631 attributes for ticks, subticks etc.; when a single list
1632 is given, it is used for ticks, subticks, etc.; a single
1633 entry is allowed without being a list; None turns off
1634 the grid
1635 - zeropathattrs are a list of stroke attributes for a grid
1636 line at axis value zero; a single entry is allowed without
1637 being a list; None turns off the zeropath
1638 - basepathattrs are a list of stroke attributes for a grid
1639 line at axis value zero; a single entry is allowed without
1640 being a list; None turns off the basepath
1641 - labeldist is a visual PyX length for the distance of the labels
1642 from the axis basepath
1643 - labelattrs is a list of attributes for a texrunners text
1644 method; a single entry is allowed without being a list;
1645 None turns off the labels
1646 - titledirection is an instance of rotatetext or None
1647 - labelhequalize and labelvequalize (booleans) perform an equal
1648 alignment for straight vertical and horizontal axes, respectively
1649 - futher keyword arguments are passed to axistitlepainter"""
1650 # TODO: access to axis.divisor -- document, remove, ... ???
1651 self.innerticklengths_str = innerticklengths
1652 self.outerticklengths_str = outerticklengths
1653 self.tickattrs = tickattrs
1654 self.gridattrs = gridattrs
1655 self.zeropathattrs = zeropathattrs
1656 self.basepathattrs = basepathattrs
1657 self.labeldist_str = labeldist
1658 self.labelattrs = labelattrs
1659 self.labeldirection = labeldirection
1660 self.labelhequalize = labelhequalize
1661 self.labelvequalize = labelvequalize
1662 axistitlepainter.__init__(self, **kwargs)
1664 def paint(self, axispos, axis, ac=None):
1665 if ac is None:
1666 ac = axiscanvas()
1667 else:
1668 raise RuntimeError("XXX") # XXX debug only
1669 labeldist = unit.length(self.labeldist_str, default_type="v")
1670 for tick in axis.ticks:
1671 tick.temp_v = axis.convert(float(tick) * axis.divisor)
1672 tick.temp_x, tick.temp_y = axispos.vtickpoint_pt(tick.temp_v)
1673 tick.temp_dx, tick.temp_dy = axispos.vtickdirection(tick.temp_v)
1675 # create & align tick.temp_labelbox
1676 for tick in axis.ticks:
1677 if tick.labellevel is not None:
1678 labelattrs = helper.getsequenceno(self.labelattrs, tick.labellevel)
1679 if labelattrs is not None:
1680 labelattrs = helper.ensurelist(labelattrs)[:]
1681 if self.labeldirection is not None:
1682 labelattrs.append(self.labeldirection.trafo(tick.temp_dx, tick.temp_dy))
1683 if tick.labelattrs is not None:
1684 labelattrs.extend(helper.ensurelist(tick.labelattrs))
1685 tick.temp_labelbox = self.texrunner.text_pt(tick.temp_x, tick.temp_y, tick.label, *labelattrs)
1686 if len(axis.ticks) > 1:
1687 equaldirection = 1
1688 for tick in axis.ticks[1:]:
1689 if tick.temp_dx != axis.ticks[0].temp_dx or tick.temp_dy != axis.ticks[0].temp_dy:
1690 equaldirection = 0
1691 else:
1692 equaldirection = 0
1693 if equaldirection and ((not axis.ticks[0].temp_dx and self.labelvequalize) or
1694 (not axis.ticks[0].temp_dy and self.labelhequalize)):
1695 if self.labelattrs is not None:
1696 box.linealignequal([tick.temp_labelbox for tick in axis.ticks if tick.labellevel is not None],
1697 labeldist, -axis.ticks[0].temp_dx, -axis.ticks[0].temp_dy)
1698 else:
1699 for tick in axis.ticks:
1700 if tick.labellevel is not None and self.labelattrs is not None:
1701 tick.temp_labelbox.linealign(labeldist, -tick.temp_dx, -tick.temp_dy)
1703 def mkv(arg):
1704 if helper.issequence(arg):
1705 return [unit.length(a, default_type="v") for a in arg]
1706 if arg is not None:
1707 return unit.length(arg, default_type="v")
1708 innerticklengths = mkv(self.innerticklengths_str)
1709 outerticklengths = mkv(self.outerticklengths_str)
1711 for tick in axis.ticks:
1712 if tick.ticklevel is not None:
1713 innerticklength = helper.getitemno(innerticklengths, tick.ticklevel)
1714 outerticklength = helper.getitemno(outerticklengths, tick.ticklevel)
1715 if innerticklength is not None or outerticklength is not None:
1716 if innerticklength is None:
1717 innerticklength = 0
1718 if outerticklength is None:
1719 outerticklength = 0
1720 tickattrs = helper.getsequenceno(self.tickattrs, tick.ticklevel)
1721 if tickattrs is not None:
1722 innerticklength_pt = unit.topt(innerticklength)
1723 outerticklength_pt = unit.topt(outerticklength)
1724 x1 = tick.temp_x + tick.temp_dx * innerticklength_pt
1725 y1 = tick.temp_y + tick.temp_dy * innerticklength_pt
1726 x2 = tick.temp_x - tick.temp_dx * outerticklength_pt
1727 y2 = tick.temp_y - tick.temp_dy * outerticklength_pt
1728 ac.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(tickattrs))
1729 if tick != frac((0, 1)) or self.zeropathattrs is None:
1730 gridattrs = helper.getsequenceno(self.gridattrs, tick.ticklevel)
1731 if gridattrs is not None:
1732 ac.stroke(axispos.vgridpath(tick.temp_v), *helper.ensuresequence(gridattrs))
1733 if outerticklength is not None and unit.topt(outerticklength) > unit.topt(ac.extent):
1734 ac.extent = outerticklength
1735 if outerticklength is not None and unit.topt(-innerticklength) > unit.topt(ac.extent):
1736 ac.extent = -innerticklength
1737 if tick.labellevel is not None and self.labelattrs is not None:
1738 ac.insert(tick.temp_labelbox)
1739 ac.labels.append(tick.temp_labelbox)
1740 extent = tick.temp_labelbox.extent(tick.temp_dx, tick.temp_dy) + labeldist
1741 if unit.topt(extent) > unit.topt(ac.extent):
1742 ac.extent = extent
1743 if self.basepathattrs is not None:
1744 ac.stroke(axispos.vbasepath(), *helper.ensuresequence(self.basepathattrs))
1745 if self.zeropathattrs is not None:
1746 if len(axis.ticks) and axis.ticks[0] * axis.ticks[-1] < frac((0, 1)):
1747 ac.stroke(axispos.gridpath(0), *helper.ensuresequence(self.zeropathattrs))
1749 # for tick in axis.ticks:
1750 # del tick.temp_v # we've inserted those temporary variables ... and do not care any longer about them
1751 # del tick.temp_x
1752 # del tick.temp_y
1753 # del tick.temp_dx
1754 # del tick.temp_dy
1755 # if tick.labellevel is not None and self.labelattrs is not None:
1756 # del tick.temp_labelbox
1758 axistitlepainter.paint(self, axispos, axis, ac=ac)
1760 return ac
1763 class linkaxispainter(axispainter):
1764 """class for painting a linked axis
1765 - the inherited axispainter is used to paint the axis
1766 - modifies some constructor defaults"""
1768 __implements__ = _Iaxispainter
1770 def __init__(self, zeropathattrs=None,
1771 labelattrs=None,
1772 titleattrs=None,
1773 **kwargs):
1774 """initializes the instance
1775 - the zeropathattrs default is set to None thus skipping the zeropath
1776 - the labelattrs default is set to None thus skipping the labels
1777 - the titleattrs default is set to None thus skipping the title
1778 - all keyword arguments are passed to axispainter"""
1779 axispainter.__init__(self, zeropathattrs=zeropathattrs,
1780 labelattrs=labelattrs,
1781 titleattrs=titleattrs,
1782 **kwargs)
1785 class subaxispos:
1786 """implementation of the _Iaxispos interface for a subaxis"""
1788 __implements__ = _Iaxispos
1790 def __init__(self, convert, baseaxispos, vmin, vmax, vminover, vmaxover):
1791 """initializes the instance
1792 - convert is the subaxis convert method
1793 - baseaxispos is the axispos instance of the base axis
1794 - vmin, vmax is the range covered by the subaxis in graph coordinates
1795 - vminover, vmaxover is the extended range of the subaxis including
1796 regions between several subaxes (for baseline drawing etc.)"""
1797 self.convert = convert
1798 self.baseaxispos = baseaxispos
1799 self.vmin = vmin
1800 self.vmax = vmax
1801 self.vminover = vminover
1802 self.vmaxover = vmaxover
1804 def basepath(self, x1=None, x2=None):
1805 if x1 is not None:
1806 v1 = self.vmin+self.convert(x1)*(self.vmax-self.vmin)
1807 else:
1808 v1 = self.vminover
1809 if x2 is not None:
1810 v2 = self.vmin+self.convert(x2)*(self.vmax-self.vmin)
1811 else:
1812 v2 = self.vmaxover
1813 return self.baseaxispos.vbasepath(v1, v2)
1815 def vbasepath(self, v1=None, v2=None):
1816 if v1 is not None:
1817 v1 = self.vmin+v1*(self.vmax-self.vmin)
1818 else:
1819 v1 = self.vminover
1820 if v2 is not None:
1821 v2 = self.vmin+v2*(self.vmax-self.vmin)
1822 else:
1823 v2 = self.vmaxover
1824 return self.baseaxispos.vbasepath(v1, v2)
1826 def gridpath(self, x):
1827 return self.baseaxispos.vgridpath(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1829 def vgridpath(self, v):
1830 return self.baseaxispos.vgridpath(self.vmin+v*(self.vmax-self.vmin))
1832 def tickpoint_pt(self, x, axis=None):
1833 return self.baseaxispos.vtickpoint_pt(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1835 def tickpoint(self, x, axis=None):
1836 return self.baseaxispos.vtickpoint(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1838 def vtickpoint_pt(self, v, axis=None):
1839 return self.baseaxispos.vtickpoint_pt(self.vmin+v*(self.vmax-self.vmin))
1841 def vtickpoint(self, v, axis=None):
1842 return self.baseaxispos.vtickpoint(self.vmin+v*(self.vmax-self.vmin))
1844 def tickdirection(self, x, axis=None):
1845 return self.baseaxispos.vtickdirection(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1847 def vtickdirection(self, v, axis=None):
1848 return self.baseaxispos.vtickdirection(self.vmin+v*(self.vmax-self.vmin))
1851 class splitaxispainter(axistitlepainter):
1852 """class for painting a splitaxis
1853 - the inherited titleaxispainter is used to paint the title of
1854 the axis
1855 - the splitaxispainter access the subaxes attribute of the axis"""
1857 __implements__ = _Iaxispainter
1859 def __init__(self, breaklinesdist="0.05 cm",
1860 breaklineslength="0.5 cm",
1861 breaklinesangle=-60,
1862 breaklinesattrs=(),
1863 **args):
1864 """initializes the instance
1865 - breaklinesdist is a visual length of the distance between
1866 the two lines of the axis break
1867 - breaklineslength is a visual length of the length of the
1868 two lines of the axis break
1869 - breaklinesangle is the angle of the lines of the axis break
1870 - breaklinesattrs are a list of stroke attributes for the
1871 axis break lines; a single entry is allowed without being a
1872 list; None turns off the break lines
1873 - futher keyword arguments are passed to axistitlepainter"""
1874 self.breaklinesdist_str = breaklinesdist
1875 self.breaklineslength_str = breaklineslength
1876 self.breaklinesangle = breaklinesangle
1877 self.breaklinesattrs = breaklinesattrs
1878 axistitlepainter.__init__(self, **args)
1880 def paint(self, axispos, axis, ac=None):
1881 if ac is None:
1882 ac = axiscanvas()
1883 else:
1884 raise RuntimeError("XXX") # XXX debug only
1885 for subaxis in axis.subaxes:
1886 subac = subaxis.finish(subaxispos(subaxis.convert, axispos, subaxis.vmin, subaxis.vmax, subaxis.vminover, subaxis.vmaxover), axis)
1887 ac.insert(subac)
1888 if unit.topt(ac.extent) < unit.topt(subac.extent):
1889 ac.extent = subac.extent
1890 if self.breaklinesattrs is not None:
1891 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1892 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1893 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1894 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1895 breaklinesextent = (0.5*self.breaklinesdist*math.fabs(self.cos) +
1896 0.5*self.breaklineslength*math.fabs(self.sin))
1897 if unit.topt(ac.extent) < unit.topt(breaklinesextent):
1898 ac.extent = breaklinesextent
1899 for subaxis1, subaxis2 in zip(axis.subaxes[:-1], axis.subaxes[1:]):
1900 # use a tangent of the basepath (this is independent of the tickdirection)
1901 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1902 p = path.normpath(axispos.vbasepath(v, None))
1903 breakline = p.tangent(0, self.breaklineslength)
1904 widthline = p.tangent(0, self.breaklinesdist).transformed(trafomodule.rotate(self.breaklinesangle+90, *breakline.begin()))
1905 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1906 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1907 breakline = breakline.transformed(trafomodule.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1908 breakline1 = breakline.transformed(trafomodule.translate(*towidth))
1909 breakline2 = breakline.transformed(trafomodule.translate(-towidth[0], -towidth[1]))
1910 ac.fill(path.path(path.moveto(*breakline1.begin()),
1911 path.lineto(*breakline1.end()),
1912 path.lineto(*breakline2.end()),
1913 path.lineto(*breakline2.begin()),
1914 path.closepath()), color.gray.white)
1915 ac.stroke(breakline1, *helper.ensuresequence(self.breaklinesattrs))
1916 ac.stroke(breakline2, *helper.ensuresequence(self.breaklinesattrs))
1917 axistitlepainter.paint(self, axispos, axis, ac=ac)
1918 return ac
1921 class linksplitaxispainter(splitaxispainter):
1922 """class for painting a linked splitaxis
1923 - the inherited splitaxispainter is used to paint the axis
1924 - modifies some constructor defaults"""
1926 __implements__ = _Iaxispainter
1928 def __init__(self, titleattrs=None, **kwargs):
1929 """initializes the instance
1930 - the titleattrs default is set to None thus skipping the title
1931 - all keyword arguments are passed to splitaxispainter"""
1932 splitaxispainter.__init__(self, titleattrs=titleattrs, **kwargs)
1935 class baraxispainter(axistitlepainter):
1936 """class for painting a baraxis
1937 - the inherited titleaxispainter is used to paint the title of
1938 the axis
1939 - the baraxispainter access the multisubaxis, subaxis names, texts, and
1940 relsizes attributes"""
1942 __implements__ = _Iaxispainter
1944 def __init__(self, innerticklength=None,
1945 outerticklength=None,
1946 tickattrs=(),
1947 basepathattrs=style.linecap.square,
1948 namedist="0.3 cm",
1949 nameattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1950 namedirection=None,
1951 namepos=0.5,
1952 namehequalize=0,
1953 namevequalize=1,
1954 **args):
1955 """initializes the instance
1956 - innerticklength and outerticklength are a visual length of
1957 the ticks to be plotted at the axis basepath to visually
1958 separate the bars; if neither innerticklength nor
1959 outerticklength are set, not ticks are plotted
1960 - breaklinesattrs are a list of stroke attributes for the
1961 axis tick; a single entry is allowed without being a
1962 list; None turns off the ticks
1963 - namedist is a visual PyX length for the distance of the bar
1964 names from the axis basepath
1965 - nameattrs is a list of attributes for a texrunners text
1966 method; a single entry is allowed without being a list;
1967 None turns off the names
1968 - namedirection is an instance of rotatetext or None
1969 - namehequalize and namevequalize (booleans) perform an equal
1970 alignment for straight vertical and horizontal axes, respectively
1971 - futher keyword arguments are passed to axistitlepainter"""
1972 self.innerticklength_str = innerticklength
1973 self.outerticklength_str = outerticklength
1974 self.tickattrs = tickattrs
1975 self.basepathattrs = basepathattrs
1976 self.namedist_str = namedist
1977 self.nameattrs = nameattrs
1978 self.namedirection = namedirection
1979 self.namepos = namepos
1980 self.namehequalize = namehequalize
1981 self.namevequalize = namevequalize
1982 axistitlepainter.__init__(self, **args)
1984 def paint(self, axispos, axis, ac=None):
1985 if ac is None:
1986 ac = axiscanvas()
1987 else:
1988 raise RuntimeError("XXX") # XXX debug only
1989 if axis.multisubaxis is not None:
1990 for subaxis in axis.subaxis:
1991 subac = subaxis.finish(subaxispos(subaxis.convert, axispos, subaxis.vmin, subaxis.vmax, None, None), axis)
1992 ac.insert(subac)
1993 if unit.topt(ac.extent) < unit.topt(subac.extent):
1994 ac.extent = subac.extent
1995 namepos = []
1996 for name in axis.names:
1997 v = axis.convert((name, self.namepos))
1998 x, y = axispos.vtickpoint_pt(v)
1999 dx, dy = axispos.vtickdirection(v)
2000 namepos.append((v, x, y, dx, dy))
2001 nameboxes = []
2002 if self.nameattrs is not None:
2003 for (v, x, y, dx, dy), name in zip(namepos, axis.names):
2004 nameattrs = helper.ensurelist(self.nameattrs)[:]
2005 if self.namedirection is not None:
2006 nameattrs.append(self.namedirection.trafo(tick.temp_dx, tick.temp_dy))
2007 if axis.texts.has_key(name):
2008 nameboxes.append(self.texrunner.text_pt(x, y, str(axis.texts[name]), *nameattrs))
2009 elif axis.texts.has_key(str(name)):
2010 nameboxes.append(self.texrunner.text_pt(x, y, str(axis.texts[str(name)]), *nameattrs))
2011 else:
2012 nameboxes.append(self.texrunner.text_pt(x, y, str(name), *nameattrs))
2013 labeldist = ac.extent + unit.length(self.namedist_str, default_type="v")
2014 if len(namepos) > 1:
2015 equaldirection = 1
2016 for np in namepos[1:]:
2017 if np[3] != namepos[0][3] or np[4] != namepos[0][4]:
2018 equaldirection = 0
2019 else:
2020 equaldirection = 0
2021 if equaldirection and ((not namepos[0][3] and self.namevequalize) or
2022 (not namepos[0][4] and self.namehequalize)):
2023 box.linealignequal(nameboxes, labeldist, -namepos[0][3], -namepos[0][4])
2024 else:
2025 for namebox, np in zip(nameboxes, namepos):
2026 namebox.linealign(labeldist, -np[3], -np[4])
2027 if self.innerticklength_str is not None:
2028 innerticklength = unit.length(self.innerticklength_str, default_type="v")
2029 innerticklength_pt = unit.topt(innerticklength)
2030 if self.tickattrs is not None and unit.topt(ac.extent) < -innerticklength_pt:
2031 ac.extent = -innerticklength
2032 elif self.outerticklength_str is not None:
2033 innerticklength = innerticklength_pt = 0
2034 if self.outerticklength_str is not None:
2035 outerticklength = unit.length(self.outerticklength_str, default_type="v")
2036 outerticklength_pt = unit.topt(outerticklength)
2037 if self.tickattrs is not None and unit.topt(ac.extent) < outerticklength_pt:
2038 ac.extent = outerticklength
2039 elif self.innerticklength_str is not None:
2040 outerticklength = outerticklength_pt = 0
2041 for (v, x, y, dx, dy), namebox in zip(namepos, nameboxes):
2042 newextent = namebox.extent(dx, dy) + labeldist
2043 if unit.topt(ac.extent) < unit.topt(newextent):
2044 ac.extent = newextent
2045 if self.tickattrs is not None and (self.innerticklength_str is not None or self.outerticklength_str is not None):
2046 for pos in axis.relsizes:
2047 if pos == axis.relsizes[0]:
2048 pos -= axis.firstdist
2049 elif pos != axis.relsizes[-1]:
2050 pos -= 0.5 * axis.dist
2051 v = pos / axis.relsizes[-1]
2052 x, y = axispos.vtickpoint_pt(v)
2053 dx, dy = axispos.vtickdirection(v)
2054 x1 = x + dx * innerticklength_pt
2055 y1 = y + dy * innerticklength_pt
2056 x2 = x - dx * outerticklength_pt
2057 y2 = y - dy * outerticklength_pt
2058 ac.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(self.tickattrs))
2059 if self.basepathattrs is not None:
2060 p = axispos.vbasepath()
2061 if p is not None:
2062 ac.stroke(p, *helper.ensuresequence(self.basepathattrs))
2063 for namebox in nameboxes:
2064 ac.insert(namebox)
2065 axistitlepainter.paint(self, axispos, axis, ac=ac)
2066 return ac
2069 class linkbaraxispainter(baraxispainter):
2070 """class for painting a linked baraxis
2071 - the inherited baraxispainter is used to paint the axis
2072 - modifies some constructor defaults"""
2074 __implements__ = _Iaxispainter
2076 def __init__(self, nameattrs=None, titleattrs=None, **kwargs):
2077 """initializes the instance
2078 - the titleattrs default is set to None thus skipping the title
2079 - the nameattrs default is set to None thus skipping the names
2080 - all keyword arguments are passed to axispainter"""
2081 baraxispainter.__init__(self, nameattrs=nameattrs, titleattrs=titleattrs, **kwargs)
2084 ################################################################################
2085 # axes
2086 ################################################################################
2089 class _Iaxis:
2090 """interface definition of a axis
2091 - an axis should implement an convert and invert method like
2092 _Imap, but this is not part of this interface definition;
2093 one possibility is to mix-in a proper map class, but special
2094 purpose axes might do something else"""
2096 def convert(self, x):
2097 "convert a value into graph coordinates"
2099 def invert(self, v):
2100 "invert a graph coordinate to a axis value"
2102 def getrelsize(self):
2103 """returns the relative size (width) of the axis
2104 - for use in splitaxis, baraxis etc.
2105 - might return None if no size is available"""
2107 def setrange(self, min=None, max=None):
2108 """set the axis data range
2109 - the type of min and max must fit to the axis
2110 - min<max; the axis might be reversed, but this is
2111 expressed internally only (min<max all the time)
2112 - the axis might not apply the change of the range
2113 (e.g. when the axis range is fixed by the user),
2114 but usually the range is extended to contain the
2115 given range
2116 - for invalid parameters (e.g. negativ values at an
2117 logarithmic axis), an exception should be raised
2118 - a RuntimeError is raised, when setrange is called
2119 after the finish method"""
2121 def getrange(self):
2122 """return data range as a tuple (min, max)
2123 - min<max; the axis might be reversed, but this is
2124 expressed internally only
2125 - a RuntimeError exception is raised when no
2126 range is available"""
2128 def finish(self, axispos):
2129 """finishes the axis
2130 - axispos implements _Iaxispos
2131 - the finish method returns an axiscanvas, which should be
2132 insertable into the graph to finally paint the axis
2133 - any modification of the axis range should be disabled after
2134 the finish method was called"""
2135 # TODO: be more specific about exceptions
2137 def createlinkaxis(self, **kwargs):
2138 """create a link axis to the axis itself
2139 - typically, a link axis is a axis, which share almost
2140 all properties with the axis it is linked to
2141 - typically, the painter gets replaced by a painter
2142 which doesn't put any text to the axis"""
2145 class _axis:
2146 """base implementation a regular axis
2147 - typical usage is to mix-in a linmap or a logmap to
2148 complete the definition"""
2150 def __init__(self, min=None, max=None, reverse=0, divisor=1,
2151 title=None, painter=axispainter(), texter=defaulttexter(),
2152 density=1, maxworse=2):
2153 """initializes the instance
2154 - min and max fix the axis minimum and maximum, respectively;
2155 they are determined by the data to be plotted, when not fixed
2156 - reverse (boolean) reverses the minimum and the maximum of
2157 the axis
2158 - numerical divisor for the axis partitioning
2159 - title is a string containing the axis title
2160 - axispainter is the axis painter (should implement _Ipainter)
2161 - texter is the texter (should implement _Itexter)
2162 - density is a global parameter for the axis paritioning and
2163 axis rating; its default is 1, but the range 0.5 to 2.5 should
2164 be usefull to get less or more ticks by the automatic axis
2165 partitioning
2166 - maxworse is a number of trials with worse tick rating
2167 before giving up (usually it should not be needed to increase
2168 this value; increasing the number will slow down the automatic
2169 axis partitioning considerably)
2170 - note that some methods of this class want to access a
2171 part and a rating attribute of the instance; those
2172 attributes should be initialized by the constructors
2173 of derived classes"""
2174 if min is not None and max is not None and min > max:
2175 min, max, reverse = max, min, not reverse
2176 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
2177 self.divisor = divisor
2178 self.title = title
2179 self.painter = painter
2180 self.texter = texter
2181 self.density = density
2182 self.maxworse = maxworse
2183 self.canconvert = 0
2184 self.finishac = None
2185 self._setrange()
2187 def _setrange(self, min=None, max=None):
2188 if not self.fixmin and min is not None and (self.min is None or min < self.min):
2189 self.min = min
2190 if not self.fixmax and max is not None and (self.max is None or max > self.max):
2191 self.max = max
2192 if None not in (self.min, self.max):
2193 self.canconvert = 1
2194 if self.reverse:
2195 self.setbasepoints(((self.min, 1), (self.max, 0)))
2196 else:
2197 self.setbasepoints(((self.min, 0), (self.max, 1)))
2199 def _getrange(self):
2200 return self.min, self.max
2202 def _forcerange(self, range):
2203 self.min, self.max = range
2204 self._setrange()
2206 def setrange(self, min=None, max=None):
2207 if self.finishac is not None:
2208 raise RuntimeError("axis was already finished")
2209 self._setrange(min, max)
2211 def getrange(self):
2212 if self.min is not None and self.max is not None:
2213 return self.min, self.max
2215 def checkfraclist(self, fracs):
2216 "orders a list of fracs, equal entries are not allowed"
2217 if not len(fracs): return []
2218 sorted = list(fracs)
2219 sorted.sort()
2220 last = sorted[0]
2221 for item in sorted[1:]:
2222 if last == item:
2223 raise ValueError("duplicate entry found")
2224 last = item
2225 return sorted
2227 def finish(self, axispos, texrunner):
2228 if self.finishac is not None:
2229 return self.finishac
2231 min, max = self.getrange()
2232 parter = parterpos = None
2233 if self.part is not None:
2234 self.part = helper.ensurelist(self.part)
2235 for p, i in zip(self.part, xrange(sys.maxint)):
2236 if hasattr(p, "defaultpart"):
2237 if parter is not None:
2238 raise RuntimeError("only one partitioner allowed")
2239 parter = p
2240 parterpos = i
2241 if parter is None:
2242 self.ticks = self.checkfraclist(self.part)
2243 else:
2244 self.part[:parterpos] = self.checkfraclist(self.part[:parterpos])
2245 self.part[parterpos+1:] = self.checkfraclist(self.part[parterpos+1:])
2246 self.ticks = _mergeticklists(_mergeticklists(self.part[:parterpos],
2247 parter.defaultpart(min/self.divisor,
2248 max/self.divisor,
2249 not self.fixmin,
2250 not self.fixmax)),
2251 self.part[parterpos+1:])
2252 else:
2253 self.ticks = []
2254 # lesspart and morepart can be called after defaultpart;
2255 # this works although some axes may share their autoparting,
2256 # because the axes are processed sequentially
2257 first = 1
2258 if parter is not None:
2259 worse = 0
2260 nextpart = parter.lesspart
2261 while nextpart is not None:
2262 newticks = nextpart()
2263 if newticks is not None:
2264 if parterpos is not None:
2265 newticks = _mergeticklists(_mergeticklists(self.part[:parterpos], newticks), self.part[parterpos+1:])
2266 if first:
2267 bestrate = self.rater.rateticks(self, self.ticks, self.density)
2268 bestrate += self.rater.raterange(self.convert(float(self.ticks[-1])/self.divisor)-
2269 self.convert(float(self.ticks[0])/self.divisor), 1)
2270 variants = [[bestrate, self.ticks]]
2271 first = 0
2272 else:
2273 newrate = self.rater.rateticks(self, newticks, self.density)
2274 newrate += self.rater.raterange(self.convert(float(self.ticks[-1])/self.divisor)-
2275 self.convert(float(self.ticks[0])/self.divisor), 1)
2276 variants.append([newrate, newticks])
2277 if newrate < bestrate:
2278 bestrate = newrate
2279 worse = 0
2280 else:
2281 worse += 1
2282 else:
2283 worse += 1
2284 if worse == self.maxworse and nextpart == parter.lesspart:
2285 worse = 0
2286 nextpart = parter.morepart
2287 if worse == self.maxworse and nextpart == parter.morepart:
2288 nextpart = None
2289 if not first:
2290 variants.sort()
2291 if self.painter is not None:
2292 i = 0
2293 bestrate = None
2294 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
2295 saverange = self._getrange()
2296 self.ticks = variants[i][1]
2297 if len(self.ticks):
2298 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2299 self.texter.labels(self.ticks)
2300 ac = self.painter.paint(axispos, self)
2301 ratelayout = self.rater.ratelayout(ac, self.density)
2302 if ratelayout is not None:
2303 variants[i][0] += ratelayout
2304 variants[i].append(ac)
2305 else:
2306 variants[i][0] = None
2307 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
2308 bestrate = variants[i][0]
2309 self._forcerange(saverange)
2310 i += 1
2311 if bestrate is None:
2312 raise RuntimeError("no valid axis partitioning found")
2313 variants = [variant for variant in variants[:i] if variant[0] is not None]
2314 variants.sort()
2315 self.ticks = variants[0][1]
2316 if len(self.ticks):
2317 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2318 ac = variants[0][2]
2319 else:
2320 if len(self.ticks):
2321 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2322 ac = axiscanvas()
2323 else:
2324 if len(self.ticks):
2325 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2326 self.texter.labels(self.ticks)
2327 ac = self.painter.paint(axispos, self)
2328 self.finishac = ac
2329 return self.finishac
2331 def createlinkaxis(self, **args):
2332 return linkaxis(self, **args)
2335 class linaxis(_axis, _linmap):
2336 """implementation of a linear axis"""
2338 __implements__ = _Iaxis
2340 def __init__(self, part=autolinpart(), rater=axisrater(), **args):
2341 """initializes the instance
2342 - the part attribute contains a list of one partitioner
2343 (a partitioner implements _Ipart) or/and some (manually
2344 set) ticks (implementing _Itick); a single entry might
2345 be passed without wrapping it into a list; the partitioner
2346 and the tick instances must fit to the type of the axis
2347 (e.g. they should be valid parameters to the axis convert
2348 method); the ticks and the partitioner results are mixed
2349 by _mergeticklists
2350 - the rater implements _Irater and is used to rate different
2351 tick lists created by the partitioner (after merging with
2352 manully set ticks)
2353 - futher keyword arguments are passed to _axis"""
2354 _axis.__init__(self, **args)
2355 if self.fixmin and self.fixmax:
2356 self.relsize = self.max - self.min
2357 self.part = part
2358 self.rater = rater
2361 class logaxis(_axis, _logmap):
2362 """implementation of a logarithmic axis"""
2364 __implements__ = _Iaxis
2366 def __init__(self, part=autologpart(), rater=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
2367 """initializes the instance
2368 - the part attribute contains a list of one partitioner
2369 (a partitioner implements _Ipart) or/and some (manually
2370 set) ticks (implementing _Itick); a single entry might
2371 be passed without wrapping it into a list; the partitioner
2372 and the tick instances must fit to the type of the axis
2373 (e.g. they should be valid parameters to the axis convert
2374 method); the ticks and the partitioner results are mixed
2375 by _mergeticklists
2376 - the rater implements _Irater and is used to rate different
2377 tick lists created by the partitioner (after merging with
2378 manully set ticks)
2379 - futher keyword arguments are passed to _axis"""
2380 _axis.__init__(self, **args)
2381 if self.fixmin and self.fixmax:
2382 self.relsize = math.log(self.max) - math.log(self.min)
2383 self.part = part
2384 self.rater = rater
2387 class linkaxis:
2388 """a axis linked to an already existing regular axis
2389 - almost all properties of the axis are "copied" from the
2390 axis this axis is linked to
2391 - usually, linked axis are used to create an axis to an
2392 existing axis with different painting properties; linked
2393 axis can be used to plot an axis twice at the opposite
2394 sides of a graphxy or even to share an axis between
2395 different graphs!"""
2397 __implements__ = _Iaxis
2399 def __init__(self, linkedaxis, painter=linkaxispainter()):
2400 """initializes the instance
2401 - it gets a axis this linkaxis is linked to
2402 - it gets a painter to be used for this linked axis"""
2403 self.linkedaxis = linkedaxis
2404 self.painter = painter
2405 self.finishac = None
2407 def __getattr__(self, attr):
2408 """access to unkown attributes are handed over to the
2409 axis this linkaxis is linked to"""
2410 return getattr(self.linkedaxis, attr)
2412 def finish(self, axispos, texrunner):
2413 """finishes the axis
2414 - instead of performing the hole finish process
2415 (paritioning, rating, etc.) just a painter call
2416 is performed"""
2417 if self.finishac is None:
2418 self.linkedaxis.finish(axispos, texrunner)
2419 self.finishac = self.painter.paint(axispos, self)
2420 return self.finishac
2423 class splitaxis:
2424 """implementation of a split axis
2425 - a split axis contains several (sub-)axes with
2426 non-overlapping data ranges -- between these subaxes
2427 the axis is "splitted"
2428 - (just to get sure: a splitaxis can contain other
2429 splitaxes as its subaxes)
2430 - a splitaxis implements the _Iaxispos for its subaxes
2431 by inheritance from _subaxispos"""
2433 __implements__ = _Iaxis, _Iaxispos
2435 def __init__(self, subaxes, splitlist=0.5, splitdist=0.1, relsizesplitdist=1,
2436 title=None, painter=splitaxispainter()):
2437 """initializes the instance
2438 - subaxes is a list of subaxes
2439 - splitlist is a list of graph coordinates, where the splitting
2440 of the main axis should be performed; a single entry (splitting
2441 two axes) doesn't need to be wrapped into a list; if the list
2442 isn't long enough for the subaxes, missing entries are considered
2443 to be None;
2444 - splitdist is the size of the splitting in graph coordinates, when
2445 the associated splitlist entry is not None
2446 - relsizesplitdist: a None entry in splitlist means, that the
2447 position of the splitting should be calculated out of the
2448 relsize values of conrtibuting subaxes (the size of the
2449 splitting is relsizesplitdist in values of the relsize values
2450 of the axes)
2451 - title is the title of the axis as a string
2452 - painter is the painter of the axis; it should be specialized to
2453 the splitaxis
2454 - the relsize of the splitaxis is the sum of the relsizes of the
2455 subaxes including the relsizesplitdist"""
2456 self.subaxes = subaxes
2457 self.painter = painter
2458 self.title = title
2459 self.splitlist = helper.ensurelist(splitlist)
2460 for subaxis in self.subaxes:
2461 subaxis.vmin = None
2462 subaxis.vmax = None
2463 self.subaxes[0].vmin = 0
2464 self.subaxes[0].vminover = None
2465 self.subaxes[-1].vmax = 1
2466 self.subaxes[-1].vmaxover = None
2467 for i in xrange(len(self.splitlist)):
2468 if self.splitlist[i] is not None:
2469 self.subaxes[i].vmax = self.splitlist[i] - 0.5*splitdist
2470 self.subaxes[i].vmaxover = self.splitlist[i]
2471 self.subaxes[i+1].vmin = self.splitlist[i] + 0.5*splitdist
2472 self.subaxes[i+1].vminover = self.splitlist[i]
2473 i = 0
2474 while i < len(self.subaxes):
2475 if self.subaxes[i].vmax is None:
2476 j = relsize = relsize2 = 0
2477 while self.subaxes[i + j].vmax is None:
2478 relsize += self.subaxes[i + j].relsize + relsizesplitdist
2479 j += 1
2480 relsize += self.subaxes[i + j].relsize
2481 vleft = self.subaxes[i].vmin
2482 vright = self.subaxes[i + j].vmax
2483 for k in range(i, i + j):
2484 relsize2 += self.subaxes[k].relsize
2485 self.subaxes[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
2486 relsize2 += 0.5 * relsizesplitdist
2487 self.subaxes[k].vmaxover = self.subaxes[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
2488 relsize2 += 0.5 * relsizesplitdist
2489 self.subaxes[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
2490 if i == 0 and i + j + 1 == len(self.subaxes):
2491 self.relsize = relsize
2492 i += j + 1
2493 else:
2494 i += 1
2496 self.fixmin = self.subaxes[0].fixmin
2497 if self.fixmin:
2498 self.min = self.subaxes[0].min
2499 self.fixmax = self.subaxes[-1].fixmax
2500 if self.fixmax:
2501 self.max = self.subaxes[-1].max
2503 self.finishac = None
2505 def getrange(self):
2506 min = self.subaxes[0].getrange()
2507 max = self.subaxes[-1].getrange()
2508 try:
2509 return min[0], max[1]
2510 except TypeError:
2511 return None
2513 def setrange(self, min, max):
2514 self.subaxes[0].setrange(min, None)
2515 self.subaxes[-1].setrange(None, max)
2517 def convert(self, value):
2518 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2519 if value < self.subaxes[0].max:
2520 return self.subaxes[0].vmin + self.subaxes[0].convert(value)*(self.subaxes[0].vmax-self.subaxes[0].vmin)
2521 for axis in self.subaxes[1:-1]:
2522 if value > axis.min and value < axis.max:
2523 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
2524 if value > self.subaxes[-1].min:
2525 return self.subaxes[-1].vmin + self.subaxes[-1].convert(value)*(self.subaxes[-1].vmax-self.subaxes[-1].vmin)
2526 raise ValueError("value couldn't be assigned to a split region")
2528 def finish(self, axispos, texrunner):
2529 if self.finishac is None:
2530 self.finishac = self.painter.paint(axispos, self)
2531 return self.finishac
2533 def createlinkaxis(self, **args):
2534 return linksplitaxis(self, **args)
2537 class linksplitaxis(linkaxis):
2538 """a splitaxis linked to an already existing splitaxis
2539 - inherits the access to a linked axis -- as before,
2540 basically only the painter is replaced
2541 - it takes care of the creation of linked axes of
2542 the subaxes"""
2544 __implements__ = _Iaxis
2546 def __init__(self, linkedaxis, painter=linksplitaxispainter(), subaxispainter=None):
2547 """initializes the instance
2548 - it gets a axis this linkaxis is linked to
2549 - it gets a painter to be used for this linked axis
2550 - it gets a list of painters to be used for the linkaxes
2551 of the subaxes; if None, the createlinkaxis of the subaxes
2552 are called without a painter parameter; if it is not a
2553 list, the subaxispainter is passed as the painter
2554 parameter to all createlinkaxis of the subaxes"""
2555 linkaxis.__init__(self, linkedaxis, painter=painter)
2556 if subaxispainter is not None:
2557 if helper.issequence(subaxispainter):
2558 if len(linkedaxis.subaxes) != len(subaxispainter):
2559 raise RuntimeError("subaxes and subaxispainter lengths do not fit")
2560 self.subaxes = [a.createlinkaxis(painter=p) for a, p in zip(linkedaxis.subaxes, subaxispainter)]
2561 else:
2562 self.subaxes = [a.createlinkaxis(painter=subaxispainter) for a in linkedaxis.subaxes]
2563 else:
2564 self.subaxes = [a.createlinkaxis() for a in linkedaxis.subaxes]
2567 class baraxis:
2568 """implementation of a axis for bar graphs
2569 - a bar axes is different from a splitaxis by the way it
2570 selects its subaxes: the convert method gets a list,
2571 where the first entry is a name selecting a subaxis out
2572 of a list; instead of the term "bar" or "subaxis" the term
2573 "item" will be used here
2574 - the baraxis stores a list of names be identify the items;
2575 the names might be of any time (strings, integers, etc.);
2576 the names can be printed as the titles for the items, but
2577 alternatively the names might be transformed by the texts
2578 dictionary, which maps a name to a text to be used to label
2579 the items in the painter
2580 - usually, there is only one subaxis, which is used as
2581 the subaxis for all items
2582 - alternatively it is also possible to use another baraxis
2583 as a multisubaxis; it is copied via the createsubaxis
2584 method whenever another subaxis is needed (by that a
2585 nested bar axis with a different number of subbars at
2586 each item can be created)
2587 - any axis can be a subaxis of a baraxis; if no subaxis
2588 is specified at all, the baraxis simulates a linear
2589 subaxis with a fixed range of 0 to 1
2590 - a splitaxis implements the _Iaxispos for its subaxes
2591 by inheritance from _subaxispos when the multisubaxis
2592 feature is turned on"""
2594 def __init__(self, subaxis=None, multisubaxis=None, title=None,
2595 dist=0.5, firstdist=None, lastdist=None, names=None,
2596 texts={}, painter=baraxispainter()):
2597 """initialize the instance
2598 - subaxis contains a axis to be used as the subaxis
2599 for all items
2600 - multisubaxis might contain another baraxis instance
2601 to be used to construct a new subaxis for each item;
2602 (by that a nested bar axis with a different number
2603 of subbars at each item can be created)
2604 - only one of subaxis or multisubaxis can be set; if neither
2605 of them is set, the baraxis behaves like having a linaxis
2606 as its subaxis with a fixed range 0 to 1
2607 - the title attribute contains the axis title as a string
2608 - the dist is a relsize to be used as the distance between
2609 the items
2610 - the firstdist and lastdist are the distance before the
2611 first and after the last item, respectively; when set
2612 to None (the default), 0.5*dist is used
2613 - names is a predefined list of names to identify the
2614 items; if set, the name list is fixed
2615 - texts is a dictionary transforming a name to a text in
2616 the painter; if a name isn't found in the dictionary
2617 it gets used itself
2618 - the relsize of the baraxis is the sum of the
2619 relsizes including all distances between the items"""
2620 self.dist = dist
2621 if firstdist is not None:
2622 self.firstdist = firstdist
2623 else:
2624 self.firstdist = 0.5 * dist
2625 if lastdist is not None:
2626 self.lastdist = lastdist
2627 else:
2628 self.lastdist = 0.5 * dist
2629 self.relsizes = None
2630 self.fixnames = 0
2631 self.names = []
2632 for name in helper.ensuresequence(names):
2633 self.setname(name)
2634 self.fixnames = names is not None
2635 self.multisubaxis = multisubaxis
2636 if self.multisubaxis is not None:
2637 if subaxis is not None:
2638 raise RuntimeError("either use subaxis or multisubaxis")
2639 self.subaxis = [self.createsubaxis() for name in self.names]
2640 else:
2641 self.subaxis = subaxis
2642 self.title = title
2643 self.fixnames = 0
2644 self.texts = texts
2645 self.painter = painter
2647 def createsubaxis(self):
2648 return baraxis(subaxis=self.multisubaxis.subaxis,
2649 multisubaxis=self.multisubaxis.multisubaxis,
2650 title=self.multisubaxis.title,
2651 dist=self.multisubaxis.dist,
2652 firstdist=self.multisubaxis.firstdist,
2653 lastdist=self.multisubaxis.lastdist,
2654 names=self.multisubaxis.names,
2655 texts=self.multisubaxis.texts,
2656 painter=self.multisubaxis.painter)
2658 def getrange(self):
2659 # TODO: we do not yet have a proper range handling for a baraxis
2660 return None
2662 def setrange(self, min=None, max=None):
2663 # TODO: we do not yet have a proper range handling for a baraxis
2664 raise RuntimeError("range handling for a baraxis is not implemented")
2666 def setname(self, name, *subnames):
2667 """add a name to identify an item at the baraxis
2668 - by using subnames, nested name definitions are
2669 possible
2670 - a style (or the user itself) might use this to
2671 insert new items into a baraxis
2672 - setting self.relsizes to None forces later recalculation"""
2673 if not self.fixnames:
2674 if name not in self.names:
2675 self.relsizes = None
2676 self.names.append(name)
2677 if self.multisubaxis is not None:
2678 self.subaxis.append(self.createsubaxis())
2679 if (not self.fixnames or name in self.names) and len(subnames):
2680 if self.multisubaxis is not None:
2681 if self.subaxis[self.names.index(name)].setname(*subnames):
2682 self.relsizes = None
2683 else:
2684 if self.subaxis.setname(*subnames):
2685 self.relsizes = None
2686 return self.relsizes is not None
2688 def updaterelsizes(self):
2689 # guess what it does: it recalculates relsize attribute
2690 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
2691 self.relsizes[-1] += self.lastdist - self.dist
2692 if self.multisubaxis is not None:
2693 subrelsize = 0
2694 for i in range(1, len(self.relsizes)):
2695 self.subaxis[i-1].updaterelsizes()
2696 subrelsize += self.subaxis[i-1].relsizes[-1]
2697 self.relsizes[i] += subrelsize
2698 else:
2699 if self.subaxis is None:
2700 subrelsize = 1
2701 else:
2702 self.subaxis.updaterelsizes()
2703 subrelsize = self.subaxis.relsizes[-1]
2704 for i in range(1, len(self.relsizes)):
2705 self.relsizes[i] += i * subrelsize
2707 def convert(self, value):
2708 """baraxis convert method
2709 - the value should be a list, where the first entry is
2710 a member of the names (set in the constructor or by the
2711 setname method); this first entry identifies an item in
2712 the baraxis
2713 - following values are passed to the appropriate subaxis
2714 convert method
2715 - when there is no subaxis, the convert method will behave
2716 like having a linaxis from 0 to 1 as subaxis"""
2717 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2718 if not self.relsizes:
2719 self.updaterelsizes()
2720 pos = self.names.index(value[0])
2721 if len(value) == 2:
2722 if self.subaxis is None:
2723 subvalue = value[1]
2724 else:
2725 if self.multisubaxis is not None:
2726 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
2727 else:
2728 subvalue = value[1] * self.subaxis.relsizes[-1]
2729 else:
2730 if self.multisubaxis is not None:
2731 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
2732 else:
2733 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
2734 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
2736 def finish(self, axispos, texrunner):
2737 if self.multisubaxis is not None:
2738 for name, subaxis in zip(self.names, self.subaxis):
2739 subaxis.vmin = self.convert((name, 0))
2740 subaxis.vmax = self.convert((name, 1))
2741 return self.painter.paint(axispos, self)
2743 def createlinkaxis(self, **args):
2744 return linkbaraxis(self, **args)
2747 class linkbaraxis(linkaxis):
2748 """a baraxis linked to an already existing baraxis
2749 - inherits the access to a linked axis -- as before,
2750 basically only the painter is replaced
2751 - it must take care of the creation of linked axes of
2752 the subaxes"""
2754 __implements__ = _Iaxis
2756 def __init__(self, linkedaxis, painter=linkbaraxispainter()):
2757 """initializes the instance
2758 - it gets a axis this linkaxis is linked to
2759 - it gets a painter to be used for this linked axis"""
2760 linkaxis.__init__(self, linkedaxis, painter=painter)
2761 if self.multisubaxis is not None:
2762 self.subaxis = [subaxis.createlinkaxis() for subaxis in self.linkedaxis.subaxis]
2763 elif self.subaxis is not None:
2764 self.subaxis = self.subaxis.createlinkaxis()
2767 ################################################################################
2768 # graph key
2769 ################################################################################
2772 # g = graph.graphxy(key=graph.key())
2773 # g.addkey(graph.key(), ...)
2776 class key:
2778 def __init__(self, dist="0.2 cm", pos = "tr", hinside = 1, vinside = 1, hdist="0.6 cm", vdist="0.4 cm",
2779 symbolwidth="0.5 cm", symbolheight="0.25 cm", symbolspace="0.2 cm",
2780 textattrs=textmodule.vshift.mathaxis):
2781 self.dist_str = dist
2782 self.pos = pos
2783 self.hinside = hinside
2784 self.vinside = vinside
2785 self.hdist_str = hdist
2786 self.vdist_str = vdist
2787 self.symbolwidth_str = symbolwidth
2788 self.symbolheight_str = symbolheight
2789 self.symbolspace_str = symbolspace
2790 self.textattrs = textattrs
2791 self.plotinfos = None
2792 if self.pos in ("tr", "rt"):
2793 self.right = 1
2794 self.top = 1
2795 elif self.pos in ("br", "rb"):
2796 self.right = 1
2797 self.top = 0
2798 elif self.pos in ("tl", "lt"):
2799 self.right = 0
2800 self.top = 1
2801 elif self.pos in ("bl", "lb"):
2802 self.right = 0
2803 self.top = 0
2804 else:
2805 raise RuntimeError("invalid pos attribute")
2807 def setplotinfos(self, *plotinfos):
2808 """set the plotinfos to be used in the key
2809 - call it exactly once
2810 - plotinfo instances with title == None are ignored"""
2811 if self.plotinfos is not None:
2812 raise RuntimeError("setplotinfo is called multiple times")
2813 self.plotinfos = [plotinfo for plotinfo in plotinfos if plotinfo.data.title is not None]
2815 def dolayout(self, graph):
2816 "creates the layout of the key"
2817 self.dist_pt = unit.topt(unit.length(self.dist_str, default_type="v"))
2818 self.hdist_pt = unit.topt(unit.length(self.hdist_str, default_type="v"))
2819 self.vdist_pt = unit.topt(unit.length(self.vdist_str, default_type="v"))
2820 self.symbolwidth_pt = unit.topt(unit.length(self.symbolwidth_str, default_type="v"))
2821 self.symbolheight_pt = unit.topt(unit.length(self.symbolheight_str, default_type="v"))
2822 self.symbolspace_pt = unit.topt(unit.length(self.symbolspace_str, default_type="v"))
2823 self.titles = []
2824 for plotinfo in self.plotinfos:
2825 self.titles.append(graph.texrunner.text_pt(0, 0, plotinfo.data.title, *helper.ensuresequence(self.textattrs)))
2826 box._tile(self.titles, self.dist_pt, 0, -1)
2827 box._linealignequal(self.titles, self.symbolwidth_pt + self.symbolspace_pt, 1, 0)
2829 def bbox(self):
2830 """return a bbox for the key
2831 method should be called after dolayout"""
2832 result = bbox.bbox()
2833 for title in self.titles:
2834 result = result + title.bbox() + bbox._bbox(0, title.center[1] - 0.5 * self.symbolheight_pt,
2835 0, title.center[1] + 0.5 * self.symbolheight_pt)
2836 return result
2838 def paint(self, c, x, y):
2839 """paint the graph key into a canvas c at the position x and y (in postscript points)
2840 - method should be called after dolayout
2841 - the x, y alignment might be calculated by the graph using:
2842 - the bbox of the key as returned by the keys bbox method
2843 - the attributes hdist_pt, vdist_pt, hinside, and vinside of the key
2844 - the dimension and geometry of the graph"""
2845 sc = c.insert(canvas.canvas(trafomodule._translate(x, y)))
2846 for plotinfo, title in zip(self.plotinfos, self.titles):
2847 plotinfo.style.key(sc, 0, -0.5 * self.symbolheight_pt + title.center[1],
2848 self.symbolwidth_pt, self.symbolheight_pt)
2849 sc.insert(title)
2852 ################################################################################
2853 # graph
2854 ################################################################################
2857 class lineaxispos:
2858 """an axispos linear along a line with a fix direction for the ticks"""
2860 __implements__ = _Iaxispos
2862 def __init__(self, convert, x1, y1, x2, y2, fixtickdirection):
2863 """initializes the instance
2864 - only the convert method is needed from the axis
2865 - x1, y1, x2, y2 are PyX length"""
2866 self.convert = convert
2867 self.x1 = x1
2868 self.y1 = y1
2869 self.x2 = x2
2870 self.y2 = y2
2871 self.x1_pt = unit.topt(x1)
2872 self.y1_pt = unit.topt(y1)
2873 self.x2_pt = unit.topt(x2)
2874 self.y2_pt = unit.topt(y2)
2875 self.fixtickdirection = fixtickdirection
2877 def vbasepath(self, v1=None, v2=None):
2878 if v1 is None:
2879 v1 = 0
2880 if v2 is None:
2881 v2 = 1
2882 return path._line((1-v1)*self.x1_pt+v1*self.x2_pt,
2883 (1-v1)*self.y1_pt+v1*self.y2_pt,
2884 (1-v2)*self.x1_pt+v2*self.x2_pt,
2885 (1-v2)*self.y1_pt+v2*self.y2_pt)
2887 def basepath(self, x1=None, x2=None):
2888 if x1 is None:
2889 v1 = 0
2890 else:
2891 v1 = self.convert(x1)
2892 if x2 is None:
2893 v2 = 1
2894 else:
2895 v2 = self.convert(x2)
2896 return path._line((1-v1)*self.x1_pt+v1*self.x2_pt,
2897 (1-v1)*self.y1_pt+v1*self.y2_pt,
2898 (1-v2)*self.x1_pt+v2*self.x2_pt,
2899 (1-v2)*self.y1_pt+v2*self.y2_pt)
2901 def gridpath(self, x):
2902 raise RuntimeError("gridpath not available")
2904 def vgridpath(self, v):
2905 raise RuntimeError("gridpath not available")
2907 def vtickpoint_pt(self, v):
2908 return (1-v)*self.x1_pt+v*self.x2_pt, (1-v)*self.y1_pt+v*self.y2_pt
2910 def vtickpoint(self, v):
2911 return (1-v)*self.x1+v*self.x2, (1-v)*self.y1+v*self.y2
2913 def tickpoint_pt(self, x):
2914 v = self.convert(x)
2915 return (1-v)*self.x1_pt+v*self.x2_pt, (1-v)*self.y1_pt+v*self.y2_pt
2917 def tickpoint(self, x):
2918 v = self.convert(x)
2919 return (1-v)*self.x1+v*self.x2, (1-v)*self.y1+v*self.y2
2921 def tickdirection(self, x):
2922 return self.fixtickdirection
2924 def vtickdirection(self, v):
2925 return self.fixtickdirection
2928 class lineaxisposlinegrid(lineaxispos):
2929 """an axispos linear along a line with a fix direction for the ticks"""
2931 __implements__ = _Iaxispos
2933 def __init__(self, convert, x1, y1, x2, y2, fixtickdirection, startgridlength, endgridlength):
2934 """initializes the instance
2935 - only the convert method is needed from the axis
2936 - x1, y1, x2, y2 are PyX length"""
2937 lineaxispos.__init__(self, convert, x1, y1, x2, y2, fixtickdirection)
2938 self.startgridlength = startgridlength
2939 self.endgridlength = endgridlength
2940 self.startgridlength_pt = unit.topt(self.startgridlength)
2941 self.endgridlength_pt = unit.topt(self.endgridlength)
2943 def gridpath(self, x):
2944 v = self.convert(x)
2945 return path._line((1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.startgridlength_pt,
2946 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.startgridlength_pt,
2947 (1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.endgridlength_pt,
2948 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.endgridlength_pt)
2950 def vgridpath(self, v):
2951 return path._line((1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.startgridlength_pt,
2952 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.startgridlength_pt,
2953 (1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.endgridlength_pt,
2954 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.endgridlength_pt)
2957 class plotinfo:
2959 def __init__(self, data, style):
2960 self.data = data
2961 self.style = style
2965 class graphxy(canvas.canvas):
2967 Names = "x", "y"
2969 class axisposdata:
2971 def __init__(self, type, axispos, tickdirection):
2973 - type == 0: x-axis; type == 1: y-axis
2974 - axispos_pt is the y or x position of the x-axis or y-axis
2975 in postscript points, respectively
2976 - axispos is analogous to axispos, but as a PyX length
2977 - dx and dy is the tick direction
2979 self.type = type
2980 self.axispos = axispos
2981 self.axispos_pt = unit.topt(axispos)
2982 self.tickdirection = tickdirection
2984 def clipcanvas(self):
2985 return self.insert(canvas.canvas(canvas.clip(path._rect(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt))))
2987 def plot(self, data, style=None):
2988 if self.haslayout:
2989 raise RuntimeError("layout setup was already performed")
2990 if style is None:
2991 if helper.issequence(data):
2992 raise RuntimeError("list plot needs an explicit style")
2993 if self.defaultstyle.has_key(data.defaultstyle):
2994 style = self.defaultstyle[data.defaultstyle].iterate()
2995 else:
2996 style = data.defaultstyle()
2997 self.defaultstyle[data.defaultstyle] = style
2998 plotinfos = []
2999 first = 1
3000 for d in helper.ensuresequence(data):
3001 if not first:
3002 style = style.iterate()
3003 first = 0
3004 if d is not None:
3005 d.setstyle(self, style)
3006 plotinfos.append(plotinfo(d, style))
3007 self.plotinfos.extend(plotinfos)
3008 if helper.issequence(data):
3009 return plotinfos
3010 return plotinfos[0]
3012 def addkey(self, key, *plotinfos):
3013 if self.haslayout:
3014 raise RuntimeError("layout setup was already performed")
3015 self.addkeys.append((key, plotinfos))
3017 def pos_pt(self, x, y, xaxis=None, yaxis=None):
3018 if xaxis is None:
3019 xaxis = self.axes["x"]
3020 if yaxis is None:
3021 yaxis = self.axes["y"]
3022 return self.xpos_pt+xaxis.convert(x)*self.width_pt, self.ypos_pt+yaxis.convert(y)*self.height_pt
3024 def pos(self, x, y, xaxis=None, yaxis=None):
3025 if xaxis is None:
3026 xaxis = self.axes["x"]
3027 if yaxis is None:
3028 yaxis = self.axes["y"]
3029 return self.xpos+xaxis.convert(x)*self.width, self.ypos+yaxis.convert(y)*self.height
3031 def vpos_pt(self, vx, vy):
3032 return self.xpos_pt+vx*self.width_pt, self.ypos_pt+vy*self.height_pt
3034 def vpos(self, vx, vy):
3035 return self.xpos+vx*self.width, self.ypos+vy*self.height
3037 # def xbaseline(self, x1=None, x2=None, axis=None):
3038 # if axis is None:
3039 # axis = self.axes["x"]
3040 # if x1 is not None:
3041 # v1 = axis.convert(x1)
3042 # else:
3043 # v1 = 0
3044 # if x2 is not None:
3045 # v2 = axis.convert(x2)
3046 # else:
3047 # v2 = 1
3048 # return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3049 # self._xpos+v2*self._width, axis.axisposdata._axispos)
3051 # def ybaseline(self, x1=None, x2=None, axis=None):
3052 # if axis is None:
3053 # axis = self.axes["y"]
3054 # if x1 is not None:
3055 # v1 = axis.convert(x1)
3056 # else:
3057 # v1 = 0
3058 # if x2 is not None:
3059 # v2 = axis.convert(x2)
3060 # else:
3061 # v2 = 1
3062 # return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3063 # axis.axisposdata._axispos, self._ypos+v2*self._height)
3065 # def vxbaseline(self, v1=None, v2=None, axis=None):
3066 # if axis is None:
3067 # axis = self.axes["x"]
3068 # if v1 is None:
3069 # v1 = 0
3070 # if v2 is None:
3071 # v2 = 1
3072 # return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3073 # self._xpos+v2*self._width, axis.axisposdata._axispos)
3075 # def vybaseline(self, v1=None, v2=None, axis=None):
3076 # if axis is None:
3077 # axis = self.axes["y"]
3078 # if v1 is None:
3079 # v1 = 0
3080 # if v2 is None:
3081 # v2 = 1
3082 # return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3083 # axis.axisposdata._axispos, self._ypos+v2*self._height)
3085 # def xgridline(self, x, axis=None):
3086 # if axis is None:
3087 # axis = self.axes["x"]
3088 # v = axis.convert(x)
3089 # return path._line(self._xpos+v*self._width, self._ypos,
3090 # self._xpos+v*self._width, self._ypos+self._height)
3092 # def ygridline(self, x, axis=None):
3093 # if axis is None:
3094 # axis = self.axes["y"]
3095 # v = axis.convert(x)
3096 # return path._line(self._xpos, self._ypos+v*self._height,
3097 # self._xpos+self._width, self._ypos+v*self._height)
3099 # def vxgridline(self, v, axis=None):
3100 # if axis is None:
3101 # axis = self.axes["x"]
3102 # return path._line(self._xpos+v*self._width, self._ypos,
3103 # self._xpos+v*self._width, self._ypos+self._height)
3105 # def vygridline(self, v, axis=None):
3106 # if axis is None:
3107 # axis = self.axes["y"]
3108 # return path._line(self._xpos, self._ypos+v*self._height,
3109 # self._xpos+self._width, self._ypos+v*self._height)
3111 # def _xtickpoint(self, x, axis=None):
3112 # if axis is None:
3113 # axis = self.axes["x"]
3114 # return self._xpos+axis.convert(x)*self._width, axis.axisposdata._axispos
3116 # def _ytickpoint(self, x, axis=None):
3117 # if axis is None:
3118 # axis = self.axes["y"]
3119 # return axis.axisposdata._axispos, self._ypos+axis.convert(x)*self._height
3121 # def xtickpoint(self, x, axis=None):
3122 # if axis is None:
3123 # axis = self.axes["x"]
3124 # return self.xpos+axis.convert(x)*self.width, axis.axisposdata.axispos
3126 # def ytickpoint(self, x, axis=None):
3127 # if axis is None:
3128 # axis = self.axes["y"]
3129 # return axis.axisposdata.axispos, self.ypos+axis.convert(x)*self.height
3131 # def _vxtickpoint(self, v, axis=None):
3132 # if axis is None:
3133 # axis = self.axes["x"]
3134 # return self._xpos+v*self._width, axis.axisposdata._axispos
3136 # def _vytickpoint(self, v, axis=None):
3137 # if axis is None:
3138 # axis = self.axes["y"]
3139 # return axis.axisposdata._axispos, self._ypos+v*self._height
3141 # def vxtickpoint(self, v, axis=None):
3142 # if axis is None:
3143 # axis = self.axes["x"]
3144 # return self.xpos+v*self.width, axis.axisposdata.axispos
3146 # def vytickpoint(self, v, axis=None):
3147 # if axis is None:
3148 # axis = self.axes["y"]
3149 # return axis.axisposdata.axispos, self.ypos+v*self.height
3151 # def xtickdirection(self, x, axis=None):
3152 # if axis is None:
3153 # axis = self.axes["x"]
3154 # return axis.axisposdata.tickdirection
3156 # def ytickdirection(self, x, axis=None):
3157 # if axis is None:
3158 # axis = self.axes["y"]
3159 # return axis.axisposdata.tickdirection
3161 # def vxtickdirection(self, v, axis=None):
3162 # if axis is None:
3163 # axis = self.axes["x"]
3164 # return axis.axisposdata.tickdirection
3166 # def vytickdirection(self, v, axis=None):
3167 # if axis is None:
3168 # axis = self.axes["y"]
3169 # return axis.axisposdata.tickdirection
3171 def _addpos(self, x, y, dx, dy):
3172 return x+dx, y+dy
3174 def _connect(self, x1, y1, x2, y2):
3175 return path._lineto(x2, y2)
3177 def keynum(self, key):
3178 try:
3179 while key[0] in string.letters:
3180 key = key[1:]
3181 return int(key)
3182 except IndexError:
3183 return 1
3185 def gatherranges(self):
3186 ranges = {}
3187 for plotinfo in self.plotinfos:
3188 pdranges = plotinfo.data.getranges()
3189 if pdranges is not None:
3190 for key in pdranges.keys():
3191 if key not in ranges.keys():
3192 ranges[key] = pdranges[key]
3193 else:
3194 ranges[key] = (min(ranges[key][0], pdranges[key][0]),
3195 max(ranges[key][1], pdranges[key][1]))
3196 # known ranges are also set as ranges for the axes
3197 for key, axis in self.axes.items():
3198 if key in ranges.keys():
3199 axis.setrange(*ranges[key])
3200 ranges[key] = axis.getrange()
3201 if ranges[key] is None:
3202 del ranges[key]
3203 return ranges
3205 def removedomethod(self, method):
3206 hadmethod = 0
3207 while 1:
3208 try:
3209 self.domethods.remove(method)
3210 hadmethod = 1
3211 except ValueError:
3212 return hadmethod
3214 def dolayout(self):
3215 if not self.removedomethod(self.dolayout): return
3216 self.haslayout = 1
3217 # create list of ranges
3218 # 1. gather ranges
3219 ranges = self.gatherranges()
3220 # 2. calculate additional ranges out of known ranges
3221 for plotinfo in self.plotinfos:
3222 plotinfo.data.setranges(ranges)
3223 # 3. gather ranges again
3224 self.gatherranges()
3225 # do the layout for all axes
3226 axesdist = unit.length(self.axesdist_str, default_type="v")
3227 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3228 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3229 xaxisextents = [0, 0]
3230 yaxisextents = [0, 0]
3231 needxaxisdist = [0, 0]
3232 needyaxisdist = [0, 0]
3233 items = list(self.axes.items())
3234 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3235 for key, axis in items:
3236 num = self.keynum(key)
3237 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3238 num3 = 2 * (num % 2) - 1 # x1 -> 1, x2 -> -1, x3 -> 1, x4 -> -1, ...
3239 if XPattern.match(key):
3240 if needxaxisdist[num2]:
3241 xaxisextents[num2] += axesdist
3242 self.axespos[key] = lineaxisposlinegrid(self.axes[key].convert,
3243 self.xpos,
3244 self.ypos + num2*self.height - num3*xaxisextents[num2],
3245 self.xpos + self.width,
3246 self.ypos + num2*self.height - num3*xaxisextents[num2],
3247 (0, num3),
3248 xaxisextents[num2], xaxisextents[num2] + self.height)
3249 elif YPattern.match(key):
3250 if needyaxisdist[num2]:
3251 yaxisextents[num2] += axesdist
3252 self.axespos[key] = lineaxisposlinegrid(self.axes[key].convert,
3253 self.xpos + num2*self.width - num3*yaxisextents[num2],
3254 self.ypos,
3255 self.xpos + num2*self.width - num3*yaxisextents[num2],
3256 self.ypos + self.height,
3257 (num3, 0),
3258 yaxisextents[num2], yaxisextents[num2] + self.width)
3259 else:
3260 raise ValueError("Axis key '%s' not allowed" % key)
3261 self.axescanvas[key] = axis.finish(self.axespos[key], self.texrunner)
3262 if XPattern.match(key):
3263 xaxisextents[num2] += self.axescanvas[key].extent
3264 needxaxisdist[num2] = 1
3265 if YPattern.match(key):
3266 yaxisextents[num2] += self.axescanvas[key].extent
3267 needyaxisdist[num2] = 1
3269 def dobackground(self):
3270 self.dolayout()
3271 if not self.removedomethod(self.dobackground): return
3272 if self.backgroundattrs is not None:
3273 self.draw(path._rect(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt),
3274 *helper.ensuresequence(self.backgroundattrs))
3276 def doaxes(self):
3277 self.dolayout()
3278 if not self.removedomethod(self.doaxes): return
3279 for axiscanvas in self.axescanvas.values():
3280 self.insert(axiscanvas)
3282 def dodata(self):
3283 self.dolayout()
3284 if not self.removedomethod(self.dodata): return
3285 for plotinfo in self.plotinfos:
3286 plotinfo.data.draw(self)
3288 def _dokey(self, key, *plotinfos):
3289 key.setplotinfos(*plotinfos)
3290 key.dolayout(self)
3291 bbox = key.bbox()
3292 if key.right:
3293 if key.hinside:
3294 x = self.xpos_pt + self.width_pt - bbox.urx - key.hdist_pt
3295 else:
3296 x = self.xpos_pt + self.width_pt - bbox.llx + key.hdist_pt
3297 else:
3298 if key.hinside:
3299 x = self.xpos_pt - bbox.llx + key.hdist_pt
3300 else:
3301 x = self.xpos_pt - bbox.urx - key.hdist_pt
3302 if key.top:
3303 if key.vinside:
3304 y = self.ypos_pt + self.height_pt - bbox.ury - key.vdist_pt
3305 else:
3306 y = self.ypos_pt + self.height_pt - bbox.lly + key.vdist_pt
3307 else:
3308 if key.vinside:
3309 y = self.ypos_pt - bbox.lly + key.vdist_pt
3310 else:
3311 y = self.ypos_pt - bbox.ury - key.vdist_pt
3312 key.paint(self, x, y)
3314 def dokey(self):
3315 self.dolayout()
3316 if not self.removedomethod(self.dokey): return
3317 if self.key is not None:
3318 self._dokey(self.key, *self.plotinfos)
3319 for key, plotinfos in self.addkeys:
3320 self._dokey(key, *plotinfos)
3322 def finish(self):
3323 while len(self.domethods):
3324 self.domethods[0]()
3326 def initwidthheight(self, width, height, ratio):
3327 if (width is not None) and (height is None):
3328 self.width = unit.length(width)
3329 self.height = (1.0/ratio) * self.width
3330 elif (height is not None) and (width is None):
3331 self.height = unit.length(height)
3332 self.width = ratio * self.height
3333 else:
3334 self.width = unit.length(width)
3335 self.height = unit.length(height)
3336 self.width_pt = unit.topt(self.width)
3337 self.height_pt = unit.topt(self.height)
3338 if self.width_pt <= 0: raise ValueError("width <= 0")
3339 if self.height_pt <= 0: raise ValueError("height <= 0")
3341 def initaxes(self, axes, addlinkaxes=0):
3342 for key in self.Names:
3343 if not axes.has_key(key):
3344 axes[key] = linaxis()
3345 elif axes[key] is None:
3346 del axes[key]
3347 if addlinkaxes:
3348 if not axes.has_key(key + "2") and axes.has_key(key):
3349 axes[key + "2"] = axes[key].createlinkaxis()
3350 elif axes[key + "2"] is None:
3351 del axes[key + "2"]
3352 self.axes = axes
3354 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
3355 key=None, backgroundattrs=None, axesdist="0.8 cm", **axes):
3356 canvas.canvas.__init__(self)
3357 self.xpos = unit.length(xpos)
3358 self.ypos = unit.length(ypos)
3359 self.xpos_pt = unit.topt(self.xpos)
3360 self.ypos_pt = unit.topt(self.ypos)
3361 self.initwidthheight(width, height, ratio)
3362 self.initaxes(axes, 1)
3363 self.axescanvas = {}
3364 self.axespos = {}
3365 self.key = key
3366 self.backgroundattrs = backgroundattrs
3367 self.axesdist_str = axesdist
3368 self.plotinfos = []
3369 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata, self.dokey]
3370 self.haslayout = 0
3371 self.defaultstyle = {}
3372 self.addkeys = []
3374 def bbox(self):
3375 self.finish()
3376 return canvas.canvas.bbox(self)
3378 def write(self, file):
3379 self.finish()
3380 canvas.canvas.write(self, file)
3384 # some thoughts, but deferred right now
3386 # class graphxyz(graphxy):
3388 # Names = "x", "y", "z"
3390 # def _vxtickpoint(self, axis, v):
3391 # return self._vpos(v, axis.vypos, axis.vzpos)
3393 # def _vytickpoint(self, axis, v):
3394 # return self._vpos(axis.vxpos, v, axis.vzpos)
3396 # def _vztickpoint(self, axis, v):
3397 # return self._vpos(axis.vxpos, axis.vypos, v)
3399 # def vxtickdirection(self, axis, v):
3400 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
3401 # x2, y2 = self._vpos(v, 0.5, 0)
3402 # dx, dy = x1 - x2, y1 - y2
3403 # norm = math.sqrt(dx*dx + dy*dy)
3404 # return dx/norm, dy/norm
3406 # def vytickdirection(self, axis, v):
3407 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
3408 # x2, y2 = self._vpos(0.5, v, 0)
3409 # dx, dy = x1 - x2, y1 - y2
3410 # norm = math.sqrt(dx*dx + dy*dy)
3411 # return dx/norm, dy/norm
3413 # def vztickdirection(self, axis, v):
3414 # return -1, 0
3415 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
3416 # x2, y2 = self._vpos(0.5, 0.5, v)
3417 # dx, dy = x1 - x2, y1 - y2
3418 # norm = math.sqrt(dx*dx + dy*dy)
3419 # return dx/norm, dy/norm
3421 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3422 # if xaxis is None: xaxis = self.axes["x"]
3423 # if yaxis is None: yaxis = self.axes["y"]
3424 # if zaxis is None: zaxis = self.axes["z"]
3425 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3427 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3428 # if xaxis is None: xaxis = self.axes["x"]
3429 # if yaxis is None: yaxis = self.axes["y"]
3430 # if zaxis is None: zaxis = self.axes["z"]
3431 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3433 # def _vpos(self, vx, vy, vz):
3434 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
3435 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
3436 # + self.a[2]*self.b[0]*(y-self.eye[1])
3437 # + self.a[1]*self.b[2]*(x-self.eye[0])
3438 # - self.a[2]*self.b[1]*(x-self.eye[0])
3439 # - self.a[0]*self.b[2]*(y-self.eye[1])
3440 # - self.a[1]*self.b[0]*(z-self.eye[2]))
3441 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
3442 # + self.eye[2]*self.b[0]*(y-self.eye[1])
3443 # + self.eye[1]*self.b[2]*(x-self.eye[0])
3444 # - self.eye[2]*self.b[1]*(x-self.eye[0])
3445 # - self.eye[0]*self.b[2]*(y-self.eye[1])
3446 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
3447 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
3448 # + self.a[2]*self.eye[0]*(y-self.eye[1])
3449 # + self.a[1]*self.eye[2]*(x-self.eye[0])
3450 # - self.a[2]*self.eye[1]*(x-self.eye[0])
3451 # - self.a[0]*self.eye[2]*(y-self.eye[1])
3452 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
3453 # return da/d0 + self._xpos, db/d0 + self._ypos
3455 # def vpos(self, vx, vy, vz):
3456 # tx, ty = self._vpos(vx, vy, vz)
3457 # return unit.t_pt(tx), unit.t_pt(ty)
3459 # def xbaseline(self, axis, x1, x2, xaxis=None):
3460 # if xaxis is None: xaxis = self.axes["x"]
3461 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2))
3463 # def ybaseline(self, axis, y1, y2, yaxis=None):
3464 # if yaxis is None: yaxis = self.axes["y"]
3465 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2))
3467 # def zbaseline(self, axis, z1, z2, zaxis=None):
3468 # if zaxis is None: zaxis = self.axes["z"]
3469 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2))
3471 # def vxbaseline(self, axis, v1, v2):
3472 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
3473 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
3474 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
3475 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
3477 # def vybaseline(self, axis, v1, v2):
3478 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
3479 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
3480 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
3481 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
3483 # def vzbaseline(self, axis, v1, v2):
3484 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
3485 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
3486 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
3487 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
3489 # def xgridpath(self, x, xaxis=None):
3490 # assert 0
3491 # if xaxis is None: xaxis = self.axes["x"]
3492 # v = xaxis.convert(x)
3493 # return path._line(self._xpos+v*self._width, self._ypos,
3494 # self._xpos+v*self._width, self._ypos+self._height)
3496 # def ygridpath(self, y, yaxis=None):
3497 # assert 0
3498 # if yaxis is None: yaxis = self.axes["y"]
3499 # v = yaxis.convert(y)
3500 # return path._line(self._xpos, self._ypos+v*self._height,
3501 # self._xpos+self._width, self._ypos+v*self._height)
3503 # def zgridpath(self, z, zaxis=None):
3504 # assert 0
3505 # if zaxis is None: zaxis = self.axes["z"]
3506 # v = zaxis.convert(z)
3507 # return path._line(self._xpos, self._zpos+v*self._height,
3508 # self._xpos+self._width, self._zpos+v*self._height)
3510 # def vxgridpath(self, v):
3511 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
3512 # path._lineto(*self._vpos(v, 0, 1)),
3513 # path._lineto(*self._vpos(v, 1, 1)),
3514 # path._lineto(*self._vpos(v, 1, 0)),
3515 # path.closepath())
3517 # def vygridpath(self, v):
3518 # return path.path(path._moveto(*self._vpos(0, v, 0)),
3519 # path._lineto(*self._vpos(0, v, 1)),
3520 # path._lineto(*self._vpos(1, v, 1)),
3521 # path._lineto(*self._vpos(1, v, 0)),
3522 # path.closepath())
3524 # def vzgridpath(self, v):
3525 # return path.path(path._moveto(*self._vpos(0, 0, v)),
3526 # path._lineto(*self._vpos(0, 1, v)),
3527 # path._lineto(*self._vpos(1, 1, v)),
3528 # path._lineto(*self._vpos(1, 0, v)),
3529 # path.closepath())
3531 # def _addpos(self, x, y, dx, dy):
3532 # assert 0
3533 # return x+dx, y+dy
3535 # def _connect(self, x1, y1, x2, y2):
3536 # assert 0
3537 # return path._lineto(x2, y2)
3539 # def doaxes(self):
3540 # self.dolayout()
3541 # if not self.removedomethod(self.doaxes): return
3542 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
3543 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3544 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3545 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[2])
3546 # items = list(self.axes.items())
3547 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3548 # for key, axis in items:
3549 # num = self.keynum(key)
3550 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3551 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3552 # if XPattern.match(key):
3553 # axis.vypos = 0
3554 # axis.vzpos = 0
3555 # axis._vtickpoint = self._vxtickpoint
3556 # axis.vgridpath = self.vxgridpath
3557 # axis.vbaseline = self.vxbaseline
3558 # axis.vtickdirection = self.vxtickdirection
3559 # elif YPattern.match(key):
3560 # axis.vxpos = 0
3561 # axis.vzpos = 0
3562 # axis._vtickpoint = self._vytickpoint
3563 # axis.vgridpath = self.vygridpath
3564 # axis.vbaseline = self.vybaseline
3565 # axis.vtickdirection = self.vytickdirection
3566 # elif ZPattern.match(key):
3567 # axis.vxpos = 0
3568 # axis.vypos = 0
3569 # axis._vtickpoint = self._vztickpoint
3570 # axis.vgridpath = self.vzgridpath
3571 # axis.vbaseline = self.vzbaseline
3572 # axis.vtickdirection = self.vztickdirection
3573 # else:
3574 # raise ValueError("Axis key '%s' not allowed" % key)
3575 # if axis.painter is not None:
3576 # axis.dopaint(self)
3577 # # if XPattern.match(key):
3578 # # self._xaxisextents[num2] += axis._extent
3579 # # needxaxisdist[num2] = 1
3580 # # if YPattern.match(key):
3581 # # self._yaxisextents[num2] += axis._extent
3582 # # needyaxisdist[num2] = 1
3584 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
3585 # phi=30, theta=30, distance=1,
3586 # backgroundattrs=None, axesdist="0.8 cm", **axes):
3587 # canvas.canvas.__init__(self)
3588 # self.tex = tex
3589 # self.xpos = xpos
3590 # self.ypos = ypos
3591 # self._xpos = unit.topt(xpos)
3592 # self._ypos = unit.topt(ypos)
3593 # self._width = unit.topt(width)
3594 # self._height = unit.topt(height)
3595 # self._depth = unit.topt(depth)
3596 # self.width = width
3597 # self.height = height
3598 # self.depth = depth
3599 # if self._width <= 0: raise ValueError("width < 0")
3600 # if self._height <= 0: raise ValueError("height < 0")
3601 # if self._depth <= 0: raise ValueError("height < 0")
3602 # self._distance = distance*math.sqrt(self._width*self._width+
3603 # self._height*self._height+
3604 # self._depth*self._depth)
3605 # phi *= -math.pi/180
3606 # theta *= math.pi/180
3607 # self.a = (-math.sin(phi), math.cos(phi), 0)
3608 # self.b = (-math.cos(phi)*math.sin(theta),
3609 # -math.sin(phi)*math.sin(theta),
3610 # math.cos(theta))
3611 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
3612 # self._distance*math.sin(phi)*math.cos(theta),
3613 # self._distance*math.sin(theta))
3614 # self.initaxes(axes)
3615 # self.axesdist_str = axesdist
3616 # self.backgroundattrs = backgroundattrs
3618 # self.data = []
3619 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
3620 # self.haslayout = 0
3621 # self.defaultstyle = {}
3623 # def bbox(self):
3624 # self.finish()
3625 # return bbox._bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
3628 ################################################################################
3629 # attr changers
3630 ################################################################################
3633 #class _Ichangeattr:
3634 # """attribute changer
3635 # is an iterator for attributes where an attribute
3636 # is not refered by just a number (like for a sequence),
3637 # but also by the number of attributes requested
3638 # by calls of the next method (like for an color palette)
3639 # (you should ensure to call all needed next before the attr)
3641 # the attribute itself is implemented by overloading the _attr method"""
3643 # def attr(self):
3644 # "get an attribute"
3646 # def next(self):
3647 # "get an attribute changer for the next attribute"
3650 class _changeattr: pass
3653 class changeattr(_changeattr):
3655 def __init__(self):
3656 self.counter = 1
3658 def getattr(self):
3659 return self.attr(0)
3661 def iterate(self):
3662 newindex = self.counter
3663 self.counter += 1
3664 return refattr(self, newindex)
3667 class refattr(_changeattr):
3669 def __init__(self, ref, index):
3670 self.ref = ref
3671 self.index = index
3673 def getattr(self):
3674 return self.ref.attr(self.index)
3676 def iterate(self):
3677 return self.ref.iterate()
3680 # helper routines for a using attrs
3682 def _getattr(attr):
3683 "get attr out of a attr/changeattr"
3684 if isinstance(attr, _changeattr):
3685 return attr.getattr()
3686 return attr
3689 def _getattrs(attrs):
3690 "get attrs out of a list of attr/changeattr"
3691 if attrs is not None:
3692 result = []
3693 for attr in helper.ensuresequence(attrs):
3694 if isinstance(attr, _changeattr):
3695 attr = attr.getattr()
3696 if attr is not None:
3697 result.append(attr)
3698 if len(result) or not len(attrs):
3699 return result
3702 def _iterateattr(attr):
3703 "perform next to a attr/changeattr"
3704 if isinstance(attr, _changeattr):
3705 return attr.iterate()
3706 return attr
3709 def _iterateattrs(attrs):
3710 "perform next to a list of attr/changeattr"
3711 if attrs is not None:
3712 result = []
3713 for attr in helper.ensuresequence(attrs):
3714 if isinstance(attr, _changeattr):
3715 result.append(attr.iterate())
3716 else:
3717 result.append(attr)
3718 return result
3721 class changecolor(changeattr):
3723 def __init__(self, palette):
3724 changeattr.__init__(self)
3725 self.palette = palette
3727 def attr(self, index):
3728 if self.counter != 1:
3729 return self.palette.getcolor(index/float(self.counter-1))
3730 else:
3731 return self.palette.getcolor(0)
3734 class _changecolorgray(changecolor):
3736 def __init__(self, palette=color.palette.Gray):
3737 changecolor.__init__(self, palette)
3739 _changecolorgrey = _changecolorgray
3742 class _changecolorreversegray(changecolor):
3744 def __init__(self, palette=color.palette.ReverseGray):
3745 changecolor.__init__(self, palette)
3747 _changecolorreversegrey = _changecolorreversegray
3750 class _changecolorredblack(changecolor):
3752 def __init__(self, palette=color.palette.RedBlack):
3753 changecolor.__init__(self, palette)
3756 class _changecolorblackred(changecolor):
3758 def __init__(self, palette=color.palette.BlackRed):
3759 changecolor.__init__(self, palette)
3762 class _changecolorredwhite(changecolor):
3764 def __init__(self, palette=color.palette.RedWhite):
3765 changecolor.__init__(self, palette)
3768 class _changecolorwhitered(changecolor):
3770 def __init__(self, palette=color.palette.WhiteRed):
3771 changecolor.__init__(self, palette)
3774 class _changecolorgreenblack(changecolor):
3776 def __init__(self, palette=color.palette.GreenBlack):
3777 changecolor.__init__(self, palette)
3780 class _changecolorblackgreen(changecolor):
3782 def __init__(self, palette=color.palette.BlackGreen):
3783 changecolor.__init__(self, palette)
3786 class _changecolorgreenwhite(changecolor):
3788 def __init__(self, palette=color.palette.GreenWhite):
3789 changecolor.__init__(self, palette)
3792 class _changecolorwhitegreen(changecolor):
3794 def __init__(self, palette=color.palette.WhiteGreen):
3795 changecolor.__init__(self, palette)
3798 class _changecolorblueblack(changecolor):
3800 def __init__(self, palette=color.palette.BlueBlack):
3801 changecolor.__init__(self, palette)
3804 class _changecolorblackblue(changecolor):
3806 def __init__(self, palette=color.palette.BlackBlue):
3807 changecolor.__init__(self, palette)
3810 class _changecolorbluewhite(changecolor):
3812 def __init__(self, palette=color.palette.BlueWhite):
3813 changecolor.__init__(self, palette)
3816 class _changecolorwhiteblue(changecolor):
3818 def __init__(self, palette=color.palette.WhiteBlue):
3819 changecolor.__init__(self, palette)
3822 class _changecolorredgreen(changecolor):
3824 def __init__(self, palette=color.palette.RedGreen):
3825 changecolor.__init__(self, palette)
3828 class _changecolorredblue(changecolor):
3830 def __init__(self, palette=color.palette.RedBlue):
3831 changecolor.__init__(self, palette)
3834 class _changecolorgreenred(changecolor):
3836 def __init__(self, palette=color.palette.GreenRed):
3837 changecolor.__init__(self, palette)
3840 class _changecolorgreenblue(changecolor):
3842 def __init__(self, palette=color.palette.GreenBlue):
3843 changecolor.__init__(self, palette)
3846 class _changecolorbluered(changecolor):
3848 def __init__(self, palette=color.palette.BlueRed):
3849 changecolor.__init__(self, palette)
3852 class _changecolorbluegreen(changecolor):
3854 def __init__(self, palette=color.palette.BlueGreen):
3855 changecolor.__init__(self, palette)
3858 class _changecolorrainbow(changecolor):
3860 def __init__(self, palette=color.palette.Rainbow):
3861 changecolor.__init__(self, palette)
3864 class _changecolorreverserainbow(changecolor):
3866 def __init__(self, palette=color.palette.ReverseRainbow):
3867 changecolor.__init__(self, palette)
3870 class _changecolorhue(changecolor):
3872 def __init__(self, palette=color.palette.Hue):
3873 changecolor.__init__(self, palette)
3876 class _changecolorreversehue(changecolor):
3878 def __init__(self, palette=color.palette.ReverseHue):
3879 changecolor.__init__(self, palette)
3882 changecolor.Gray = _changecolorgray
3883 changecolor.Grey = _changecolorgrey
3884 changecolor.Reversegray = _changecolorreversegray
3885 changecolor.Reversegrey = _changecolorreversegrey
3886 changecolor.RedBlack = _changecolorredblack
3887 changecolor.BlackRed = _changecolorblackred
3888 changecolor.RedWhite = _changecolorredwhite
3889 changecolor.WhiteRed = _changecolorwhitered
3890 changecolor.GreenBlack = _changecolorgreenblack
3891 changecolor.BlackGreen = _changecolorblackgreen
3892 changecolor.GreenWhite = _changecolorgreenwhite
3893 changecolor.WhiteGreen = _changecolorwhitegreen
3894 changecolor.BlueBlack = _changecolorblueblack
3895 changecolor.BlackBlue = _changecolorblackblue
3896 changecolor.BlueWhite = _changecolorbluewhite
3897 changecolor.WhiteBlue = _changecolorwhiteblue
3898 changecolor.RedGreen = _changecolorredgreen
3899 changecolor.RedBlue = _changecolorredblue
3900 changecolor.GreenRed = _changecolorgreenred
3901 changecolor.GreenBlue = _changecolorgreenblue
3902 changecolor.BlueRed = _changecolorbluered
3903 changecolor.BlueGreen = _changecolorbluegreen
3904 changecolor.Rainbow = _changecolorrainbow
3905 changecolor.ReverseRainbow = _changecolorreverserainbow
3906 changecolor.Hue = _changecolorhue
3907 changecolor.ReverseHue = _changecolorreversehue
3910 class changesequence(changeattr):
3911 "cycles through a list"
3913 def __init__(self, *sequence):
3914 changeattr.__init__(self)
3915 if not len(sequence):
3916 sequence = self.defaultsequence
3917 self.sequence = sequence
3919 def attr(self, index):
3920 return self.sequence[index % len(self.sequence)]
3923 class changelinestyle(changesequence):
3924 defaultsequence = (style.linestyle.solid,
3925 style.linestyle.dashed,
3926 style.linestyle.dotted,
3927 style.linestyle.dashdotted)
3930 class changestrokedfilled(changesequence):
3931 defaultsequence = (deco.stroked(), deco.filled())
3934 class changefilledstroked(changesequence):
3935 defaultsequence = (deco.filled(), deco.stroked())
3939 ################################################################################
3940 # styles
3941 ################################################################################
3944 class symbol:
3946 def cross(self, x, y):
3947 return (path._moveto(x-0.5*self.size_pt, y-0.5*self.size_pt),
3948 path._lineto(x+0.5*self.size_pt, y+0.5*self.size_pt),
3949 path._moveto(x-0.5*self.size_pt, y+0.5*self.size_pt),
3950 path._lineto(x+0.5*self.size_pt, y-0.5*self.size_pt))
3952 def plus(self, x, y):
3953 return (path._moveto(x-0.707106781*self.size_pt, y),
3954 path._lineto(x+0.707106781*self.size_pt, y),
3955 path._moveto(x, y-0.707106781*self.size_pt),
3956 path._lineto(x, y+0.707106781*self.size_pt))
3958 def square(self, x, y):
3959 return (path._moveto(x-0.5*self.size_pt, y-0.5 * self.size_pt),
3960 path._lineto(x+0.5*self.size_pt, y-0.5 * self.size_pt),
3961 path._lineto(x+0.5*self.size_pt, y+0.5 * self.size_pt),
3962 path._lineto(x-0.5*self.size_pt, y+0.5 * self.size_pt),
3963 path.closepath())
3965 def triangle(self, x, y):
3966 return (path._moveto(x-0.759835685*self.size_pt, y-0.438691337*self.size_pt),
3967 path._lineto(x+0.759835685*self.size_pt, y-0.438691337*self.size_pt),
3968 path._lineto(x, y+0.877382675*self.size_pt),
3969 path.closepath())
3971 def circle(self, x, y):
3972 return (path._arc(x, y, 0.564189583*self.size_pt, 0, 360),
3973 path.closepath())
3975 def diamond(self, x, y):
3976 return (path._moveto(x-0.537284965*self.size_pt, y),
3977 path._lineto(x, y-0.930604859*self.size_pt),
3978 path._lineto(x+0.537284965*self.size_pt, y),
3979 path._lineto(x, y+0.930604859*self.size_pt),
3980 path.closepath())
3982 def __init__(self, symbol=helper.nodefault,
3983 size="0.2 cm", symbolattrs=deco.stroked(),
3984 errorscale=0.5, errorbarattrs=(),
3985 lineattrs=None):
3986 self.size_str = size
3987 if symbol is helper.nodefault:
3988 self._symbol = changesymbol.cross()
3989 else:
3990 self._symbol = symbol
3991 self._symbolattrs = symbolattrs
3992 self.errorscale = errorscale
3993 self._errorbarattrs = errorbarattrs
3994 self._lineattrs = lineattrs
3996 def iteratedict(self):
3997 result = {}
3998 result["symbol"] = _iterateattr(self._symbol)
3999 result["size"] = _iterateattr(self.size_str)
4000 result["symbolattrs"] = _iterateattrs(self._symbolattrs)
4001 result["errorscale"] = _iterateattr(self.errorscale)
4002 result["errorbarattrs"] = _iterateattrs(self._errorbarattrs)
4003 result["lineattrs"] = _iterateattrs(self._lineattrs)
4004 return result
4006 def iterate(self):
4007 return symbol(**self.iteratedict())
4009 def othercolumnkey(self, key, index):
4010 raise ValueError("unsuitable key '%s'" % key)
4012 def setcolumns(self, graph, columns):
4013 def checkpattern(key, index, pattern, iskey, isindex):
4014 if key is not None:
4015 match = pattern.match(key)
4016 if match:
4017 if isindex is not None: raise ValueError("multiple key specification")
4018 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4019 key = None
4020 iskey = match.groups()[0]
4021 isindex = index
4022 return key, iskey, isindex
4024 self.xi = self.xmini = self.xmaxi = None
4025 self.dxi = self.dxmini = self.dxmaxi = None
4026 self.yi = self.ymini = self.ymaxi = None
4027 self.dyi = self.dymini = self.dymaxi = None
4028 self.xkey = self.ykey = None
4029 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4030 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4031 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4032 XMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
4033 YMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
4034 XMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
4035 YMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
4036 DXPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4037 DYPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4038 DXMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
4039 DYMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
4040 DXMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
4041 DYMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
4042 for key, index in columns.items():
4043 key, self.xkey, self.xi = checkpattern(key, index, XPattern, self.xkey, self.xi)
4044 key, self.ykey, self.yi = checkpattern(key, index, YPattern, self.ykey, self.yi)
4045 key, self.xkey, self.xmini = checkpattern(key, index, XMinPattern, self.xkey, self.xmini)
4046 key, self.ykey, self.ymini = checkpattern(key, index, YMinPattern, self.ykey, self.ymini)
4047 key, self.xkey, self.xmaxi = checkpattern(key, index, XMaxPattern, self.xkey, self.xmaxi)
4048 key, self.ykey, self.ymaxi = checkpattern(key, index, YMaxPattern, self.ykey, self.ymaxi)
4049 key, self.xkey, self.dxi = checkpattern(key, index, DXPattern, self.xkey, self.dxi)
4050 key, self.ykey, self.dyi = checkpattern(key, index, DYPattern, self.ykey, self.dyi)
4051 key, self.xkey, self.dxmini = checkpattern(key, index, DXMinPattern, self.xkey, self.dxmini)
4052 key, self.ykey, self.dymini = checkpattern(key, index, DYMinPattern, self.ykey, self.dymini)
4053 key, self.xkey, self.dxmaxi = checkpattern(key, index, DXMaxPattern, self.xkey, self.dxmaxi)
4054 key, self.ykey, self.dymaxi = checkpattern(key, index, DYMaxPattern, self.ykey, self.dymaxi)
4055 if key is not None:
4056 self.othercolumnkey(key, index)
4057 if None in (self.xkey, self.ykey): raise ValueError("incomplete axis specification")
4058 if (len(filter(None, (self.xmini, self.dxmini, self.dxi))) > 1 or
4059 len(filter(None, (self.ymini, self.dymini, self.dyi))) > 1 or
4060 len(filter(None, (self.xmaxi, self.dxmaxi, self.dxi))) > 1 or
4061 len(filter(None, (self.ymaxi, self.dymaxi, self.dyi))) > 1):
4062 raise ValueError("multiple errorbar definition")
4063 if ((self.xi is None and self.dxi is not None) or
4064 (self.yi is None and self.dyi is not None) or
4065 (self.xi is None and self.dxmini is not None) or
4066 (self.yi is None and self.dymini is not None) or
4067 (self.xi is None and self.dxmaxi is not None) or
4068 (self.yi is None and self.dymaxi is not None)):
4069 raise ValueError("errorbar definition start value missing")
4070 self.xaxis = graph.axes[self.xkey]
4071 self.yaxis = graph.axes[self.ykey]
4073 def minmidmax(self, point, i, mini, maxi, di, dmini, dmaxi):
4074 min = max = mid = None
4075 try:
4076 mid = point[i] + 0.0
4077 except (TypeError, ValueError):
4078 pass
4079 try:
4080 if di is not None: min = point[i] - point[di]
4081 elif dmini is not None: min = point[i] - point[dmini]
4082 elif mini is not None: min = point[mini] + 0.0
4083 except (TypeError, ValueError):
4084 pass
4085 try:
4086 if di is not None: max = point[i] + point[di]
4087 elif dmaxi is not None: max = point[i] + point[dmaxi]
4088 elif maxi is not None: max = point[maxi] + 0.0
4089 except (TypeError, ValueError):
4090 pass
4091 if mid is not None:
4092 if min is not None and min > mid: raise ValueError("minimum error in errorbar")
4093 if max is not None and max < mid: raise ValueError("maximum error in errorbar")
4094 else:
4095 if min is not None and max is not None and min > max: raise ValueError("minimum/maximum error in errorbar")
4096 return min, mid, max
4098 def keyrange(self, points, i, mini, maxi, di, dmini, dmaxi):
4099 allmin = allmax = None
4100 if filter(None, (mini, maxi, di, dmini, dmaxi)) is not None:
4101 for point in points:
4102 min, mid, max = self.minmidmax(point, i, mini, maxi, di, dmini, dmaxi)
4103 if min is not None and (allmin is None or min < allmin): allmin = min
4104 if mid is not None and (allmin is None or mid < allmin): allmin = mid
4105 if mid is not None and (allmax is None or mid > allmax): allmax = mid
4106 if max is not None and (allmax is None or max > allmax): allmax = max
4107 else:
4108 for point in points:
4109 try:
4110 value = point[i] + 0.0
4111 if allmin is None or point[i] < allmin: allmin = point[i]
4112 if allmax is None or point[i] > allmax: allmax = point[i]
4113 except (TypeError, ValueError):
4114 pass
4115 return allmin, allmax
4117 def getranges(self, points):
4118 xmin, xmax = self.keyrange(points, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4119 ymin, ymax = self.keyrange(points, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4120 return {self.xkey: (xmin, xmax), self.ykey: (ymin, ymax)}
4122 def drawerrorbar_pt(self, graph, topleft, top, topright,
4123 left, center, right,
4124 bottomleft, bottom, bottomright, point=None):
4125 if left is not None:
4126 if right is not None:
4127 left1 = graph._addpos(*(left+(0, -self.errorsize_pt)))
4128 left2 = graph._addpos(*(left+(0, self.errorsize_pt)))
4129 right1 = graph._addpos(*(right+(0, -self.errorsize_pt)))
4130 right2 = graph._addpos(*(right+(0, self.errorsize_pt)))
4131 graph.stroke(path.path(path._moveto(*left1),
4132 graph._connect(*(left1+left2)),
4133 path._moveto(*left),
4134 graph._connect(*(left+right)),
4135 path._moveto(*right1),
4136 graph._connect(*(right1+right2))),
4137 *self.errorbarattrs)
4138 elif center is not None:
4139 left1 = graph._addpos(*(left+(0, -self.errorsize_pt)))
4140 left2 = graph._addpos(*(left+(0, self.errorsize_pt)))
4141 graph.stroke(path.path(path._moveto(*left1),
4142 graph._connect(*(left1+left2)),
4143 path._moveto(*left),
4144 graph._connect(*(left+center))),
4145 *self.errorbarattrs)
4146 else:
4147 left1 = graph._addpos(*(left+(0, -self.errorsize_pt)))
4148 left2 = graph._addpos(*(left+(0, self.errorsize_pt)))
4149 left3 = graph._addpos(*(left+(self.errorsize_pt, 0)))
4150 graph.stroke(path.path(path._moveto(*left1),
4151 graph._connect(*(left1+left2)),
4152 path._moveto(*left),
4153 graph._connect(*(left+left3))),
4154 *self.errorbarattrs)
4155 if right is not None and left is None:
4156 if center is not None:
4157 right1 = graph._addpos(*(right+(0, -self.errorsize_pt)))
4158 right2 = graph._addpos(*(right+(0, self.errorsize_pt)))
4159 graph.stroke(path.path(path._moveto(*right1),
4160 graph._connect(*(right1+right2)),
4161 path._moveto(*right),
4162 graph._connect(*(right+center))),
4163 *self.errorbarattrs)
4164 else:
4165 right1 = graph._addpos(*(right+(0, -self.errorsize_pt)))
4166 right2 = graph._addpos(*(right+(0, self.errorsize_pt)))
4167 right3 = graph._addpos(*(right+(-self.errorsize_pt, 0)))
4168 graph.stroke(path.path(path._moveto(*right1),
4169 graph._connect(*(right1+right2)),
4170 path._moveto(*right),
4171 graph._connect(*(right+right3))),
4172 *self.errorbarattrs)
4174 if bottom is not None:
4175 if top is not None:
4176 bottom1 = graph._addpos(*(bottom+(-self.errorsize_pt, 0)))
4177 bottom2 = graph._addpos(*(bottom+(self.errorsize_pt, 0)))
4178 top1 = graph._addpos(*(top+(-self.errorsize_pt, 0)))
4179 top2 = graph._addpos(*(top+(self.errorsize_pt, 0)))
4180 graph.stroke(path.path(path._moveto(*bottom1),
4181 graph._connect(*(bottom1+bottom2)),
4182 path._moveto(*bottom),
4183 graph._connect(*(bottom+top)),
4184 path._moveto(*top1),
4185 graph._connect(*(top1+top2))),
4186 *self.errorbarattrs)
4187 elif center is not None:
4188 bottom1 = graph._addpos(*(bottom+(-self.errorsize_pt, 0)))
4189 bottom2 = graph._addpos(*(bottom+(self.errorsize_pt, 0)))
4190 graph.stroke(path.path(path._moveto(*bottom1),
4191 graph._connect(*(bottom1+bottom2)),
4192 path._moveto(*bottom),
4193 graph._connect(*(bottom+center))),
4194 *self.errorbarattrs)
4195 else:
4196 bottom1 = graph._addpos(*(bottom+(-self.errorsize_pt, 0)))
4197 bottom2 = graph._addpos(*(bottom+(self.errorsize_pt, 0)))
4198 bottom3 = graph._addpos(*(bottom+(0, self.errorsize_pt)))
4199 graph.stroke(path.path(path._moveto(*bottom1),
4200 graph._connect(*(bottom1+bottom2)),
4201 path._moveto(*bottom),
4202 graph._connect(*(bottom+bottom3))),
4203 *self.errorbarattrs)
4204 if top is not None and bottom is None:
4205 if center is not None:
4206 top1 = graph._addpos(*(top+(-self.errorsize_pt, 0)))
4207 top2 = graph._addpos(*(top+(self.errorsize_pt, 0)))
4208 graph.stroke(path.path(path._moveto(*top1),
4209 graph._connect(*(top1+top2)),
4210 path._moveto(*top),
4211 graph._connect(*(top+center))),
4212 *self.errorbarattrs)
4213 else:
4214 top1 = graph._addpos(*(top+(-self.errorsize_pt, 0)))
4215 top2 = graph._addpos(*(top+(self.errorsize_pt, 0)))
4216 top3 = graph._addpos(*(top+(0, -self.errorsize_pt)))
4217 graph.stroke(path.path(path._moveto(*top1),
4218 graph._connect(*(top1+top2)),
4219 path._moveto(*top),
4220 graph._connect(*(top+top3))),
4221 *self.errorbarattrs)
4222 if bottomleft is not None:
4223 if topleft is not None and bottomright is None:
4224 bottomleft1 = graph._addpos(*(bottomleft+(self.errorsize_pt, 0)))
4225 topleft1 = graph._addpos(*(topleft+(self.errorsize_pt, 0)))
4226 graph.stroke(path.path(path._moveto(*bottomleft1),
4227 graph._connect(*(bottomleft1+bottomleft)),
4228 graph._connect(*(bottomleft+topleft)),
4229 graph._connect(*(topleft+topleft1))),
4230 *self.errorbarattrs)
4231 elif bottomright is not None and topleft is None:
4232 bottomleft1 = graph._addpos(*(bottomleft+(0, self.errorsize_pt)))
4233 bottomright1 = graph._addpos(*(bottomright+(0, self.errorsize_pt)))
4234 graph.stroke(path.path(path._moveto(*bottomleft1),
4235 graph._connect(*(bottomleft1+bottomleft)),
4236 graph._connect(*(bottomleft+bottomright)),
4237 graph._connect(*(bottomright+bottomright1))),
4238 *self.errorbarattrs)
4239 elif bottomright is None and topleft is None:
4240 bottomleft1 = graph._addpos(*(bottomleft+(self.errorsize_pt, 0)))
4241 bottomleft2 = graph._addpos(*(bottomleft+(0, self.errorsize_pt)))
4242 graph.stroke(path.path(path._moveto(*bottomleft1),
4243 graph._connect(*(bottomleft1+bottomleft)),
4244 graph._connect(*(bottomleft+bottomleft2))),
4245 *self.errorbarattrs)
4246 if topright is not None:
4247 if bottomright is not None and topleft is None:
4248 topright1 = graph._addpos(*(topright+(-self.errorsize_pt, 0)))
4249 bottomright1 = graph._addpos(*(bottomright+(-self.errorsize_pt, 0)))
4250 graph.stroke(path.path(path._moveto(*topright1),
4251 graph._connect(*(topright1+topright)),
4252 graph._connect(*(topright+bottomright)),
4253 graph._connect(*(bottomright+bottomright1))),
4254 *self.errorbarattrs)
4255 elif topleft is not None and bottomright is None:
4256 topright1 = graph._addpos(*(topright+(0, -self.errorsize_pt)))
4257 topleft1 = graph._addpos(*(topleft+(0, -self.errorsize_pt)))
4258 graph.stroke(path.path(path._moveto(*topright1),
4259 graph._connect(*(topright1+topright)),
4260 graph._connect(*(topright+topleft)),
4261 graph._connect(*(topleft+topleft1))),
4262 *self.errorbarattrs)
4263 elif topleft is None and bottomright is None:
4264 topright1 = graph._addpos(*(topright+(-self.errorsize_pt, 0)))
4265 topright2 = graph._addpos(*(topright+(0, -self.errorsize_pt)))
4266 graph.stroke(path.path(path._moveto(*topright1),
4267 graph._connect(*(topright1+topright)),
4268 graph._connect(*(topright+topright2))),
4269 *self.errorbarattrs)
4270 if bottomright is not None and bottomleft is None and topright is None:
4271 bottomright1 = graph._addpos(*(bottomright+(-self.errorsize_pt, 0)))
4272 bottomright2 = graph._addpos(*(bottomright+(0, self.errorsize_pt)))
4273 graph.stroke(path.path(path._moveto(*bottomright1),
4274 graph._connect(*(bottomright1+bottomright)),
4275 graph._connect(*(bottomright+bottomright2))),
4276 *self.errorbarattrs)
4277 if topleft is not None and bottomleft is None and topright is None:
4278 topleft1 = graph._addpos(*(topleft+(self.errorsize_pt, 0)))
4279 topleft2 = graph._addpos(*(topleft+(0, -self.errorsize_pt)))
4280 graph.stroke(path.path(path._moveto(*topleft1),
4281 graph._connect(*(topleft1+topleft)),
4282 graph._connect(*(topleft+topleft2))),
4283 *self.errorbarattrs)
4284 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4285 graph.stroke(path.path(path._moveto(*bottomleft),
4286 graph._connect(*(bottomleft+bottomright)),
4287 graph._connect(*(bottomright+topright)),
4288 graph._connect(*(topright+topleft)),
4289 path.closepath()),
4290 *self.errorbarattrs)
4292 def drawsymbol_pt(self, canvas, x, y, point=None):
4293 canvas.draw(path.path(*self.symbol(self, x, y)), *self.symbolattrs)
4295 def drawsymbol(self, canvas, x, y, point=None):
4296 self.drawsymbol_pt(canvas, unit.topt(x), unit.topt(y), point)
4298 def key(self, c, x, y, width, height):
4299 if self._symbolattrs is not None:
4300 self.drawsymbol_pt(c, x + 0.5 * width, y + 0.5 * height)
4301 if self._lineattrs is not None:
4302 c.stroke(path._line(x, y + 0.5 * height, x + width, y + 0.5 * height), *self.lineattrs)
4304 def drawpoints(self, graph, points):
4305 xaxismin, xaxismax = self.xaxis.getrange()
4306 yaxismin, yaxismax = self.yaxis.getrange()
4307 self.size = unit.length(_getattr(self.size_str), default_type="v")
4308 self.size_pt = unit.topt(self.size)
4309 self.symbol = _getattr(self._symbol)
4310 self.symbolattrs = _getattrs(helper.ensuresequence(self._symbolattrs))
4311 self.errorbarattrs = _getattrs(helper.ensuresequence(self._errorbarattrs))
4312 self.errorsize_pt = self.errorscale * self.size_pt
4313 self.errorsize = self.errorscale * self.size
4314 self.lineattrs = _getattrs(helper.ensuresequence(self._lineattrs))
4315 if self._lineattrs is not None:
4316 clipcanvas = graph.clipcanvas()
4317 lineels = []
4318 haserror = filter(None, (self.xmini, self.ymini, self.xmaxi, self.ymaxi,
4319 self.dxi, self.dyi, self.dxmini, self.dymini, self.dxmaxi, self.dymaxi)) is not None
4320 moveto = 1
4321 for point in points:
4322 drawsymbol = 1
4323 xmin, x, xmax = self.minmidmax(point, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4324 ymin, y, ymax = self.minmidmax(point, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4325 if x is not None and x < xaxismin: drawsymbol = 0
4326 elif x is not None and x > xaxismax: drawsymbol = 0
4327 elif y is not None and y < yaxismin: drawsymbol = 0
4328 elif y is not None and y > yaxismax: drawsymbol = 0
4329 # elif haserror: # TODO: correct clipcanvas handling
4330 # if xmin is not None and xmin < xaxismin: drawsymbol = 0
4331 # elif xmax is not None and xmax < xaxismin: drawsymbol = 0
4332 # elif xmax is not None and xmax > xaxismax: drawsymbol = 0
4333 # elif xmin is not None and xmin > xaxismax: drawsymbol = 0
4334 # elif ymin is not None and ymin < yaxismin: drawsymbol = 0
4335 # elif ymax is not None and ymax < yaxismin: drawsymbol = 0
4336 # elif ymax is not None and ymax > yaxismax: drawsymbol = 0
4337 # elif ymin is not None and ymin > yaxismax: drawsymbol = 0
4338 xpos=ypos=topleft=top=topright=left=center=right=bottomleft=bottom=bottomright=None
4339 if x is not None and y is not None:
4340 try:
4341 center = xpos, ypos = graph.pos_pt(x, y, xaxis=self.xaxis, yaxis=self.yaxis)
4342 except (ValueError, OverflowError): # XXX: exceptions???
4343 pass
4344 if haserror:
4345 if y is not None:
4346 if xmin is not None: left = graph.pos_pt(xmin, y, xaxis=self.xaxis, yaxis=self.yaxis)
4347 if xmax is not None: right = graph.pos_pt(xmax, y, xaxis=self.xaxis, yaxis=self.yaxis)
4348 if x is not None:
4349 if ymax is not None: top = graph.pos_pt(x, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4350 if ymin is not None: bottom = graph.pos_pt(x, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4351 if x is None or y is None:
4352 if ymax is not None:
4353 if xmin is not None: topleft = graph.pos_pt(xmin, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4354 if xmax is not None: topright = graph.pos_pt(xmax, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4355 if ymin is not None:
4356 if xmin is not None: bottomleft = graph.pos_pt(xmin, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4357 if xmax is not None: bottomright = graph.pos_pt(xmax, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4358 if drawsymbol:
4359 if self._errorbarattrs is not None and haserror:
4360 self.drawerrorbar_pt(graph, topleft, top, topright,
4361 left, center, right,
4362 bottomleft, bottom, bottomright, point)
4363 if self._symbolattrs is not None and xpos is not None and ypos is not None:
4364 self.drawsymbol_pt(graph, xpos, ypos, point)
4365 if xpos is not None and ypos is not None:
4366 if moveto:
4367 lineels.append(path._moveto(xpos, ypos))
4368 moveto = 0
4369 else:
4370 lineels.append(path._lineto(xpos, ypos))
4371 else:
4372 moveto = 1
4373 self.path = path.path(*lineels)
4374 if self._lineattrs is not None:
4375 clipcanvas.stroke(self.path, *self.lineattrs)
4378 class changesymbol(changesequence): pass
4381 class _changesymbolcross(changesymbol):
4382 defaultsequence = (symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond)
4385 class _changesymbolplus(changesymbol):
4386 defaultsequence = (symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross)
4389 class _changesymbolsquare(changesymbol):
4390 defaultsequence = (symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus)
4393 class _changesymboltriangle(changesymbol):
4394 defaultsequence = (symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square)
4397 class _changesymbolcircle(changesymbol):
4398 defaultsequence = (symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle)
4401 class _changesymboldiamond(changesymbol):
4402 defaultsequence = (symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle)
4405 class _changesymbolsquaretwice(changesymbol):
4406 defaultsequence = (symbol.square, symbol.square, symbol.triangle, symbol.triangle,
4407 symbol.circle, symbol.circle, symbol.diamond, symbol.diamond)
4410 class _changesymboltriangletwice(changesymbol):
4411 defaultsequence = (symbol.triangle, symbol.triangle, symbol.circle, symbol.circle,
4412 symbol.diamond, symbol.diamond, symbol.square, symbol.square)
4415 class _changesymbolcircletwice(changesymbol):
4416 defaultsequence = (symbol.circle, symbol.circle, symbol.diamond, symbol.diamond,
4417 symbol.square, symbol.square, symbol.triangle, symbol.triangle)
4420 class _changesymboldiamondtwice(changesymbol):
4421 defaultsequence = (symbol.diamond, symbol.diamond, symbol.square, symbol.square,
4422 symbol.triangle, symbol.triangle, symbol.circle, symbol.circle)
4425 changesymbol.cross = _changesymbolcross
4426 changesymbol.plus = _changesymbolplus
4427 changesymbol.square = _changesymbolsquare
4428 changesymbol.triangle = _changesymboltriangle
4429 changesymbol.circle = _changesymbolcircle
4430 changesymbol.diamond = _changesymboldiamond
4431 changesymbol.squaretwice = _changesymbolsquaretwice
4432 changesymbol.triangletwice = _changesymboltriangletwice
4433 changesymbol.circletwice = _changesymbolcircletwice
4434 changesymbol.diamondtwice = _changesymboldiamondtwice
4437 class line(symbol):
4439 def __init__(self, lineattrs=helper.nodefault):
4440 if lineattrs is helper.nodefault:
4441 lineattrs = (changelinestyle(), style.linejoin.round)
4442 symbol.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
4445 class rect(symbol):
4447 def __init__(self, palette=color.palette.Gray):
4448 self.palette = palette
4449 self.colorindex = None
4450 symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
4452 def iterate(self):
4453 raise RuntimeError("style is not iterateable")
4455 def othercolumnkey(self, key, index):
4456 if key == "color":
4457 self.colorindex = index
4458 else:
4459 symbol.othercolumnkey(self, key, index)
4461 def drawerrorbar_pt(self, graph, topleft, top, topright,
4462 left, center, right,
4463 bottomleft, bottom, bottomright, point=None):
4464 color = point[self.colorindex]
4465 if color is not None:
4466 if color != self.lastcolor:
4467 self.rectclipcanvas.set(self.palette.getcolor(color))
4468 if bottom is not None and left is not None:
4469 bottomleft = left[0], bottom[1]
4470 if bottom is not None and right is not None:
4471 bottomright = right[0], bottom[1]
4472 if top is not None and right is not None:
4473 topright = right[0], top[1]
4474 if top is not None and left is not None:
4475 topleft = left[0], top[1]
4476 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4477 self.rectclipcanvas.fill(path.path(path._moveto(*bottomleft),
4478 graph._connect(*(bottomleft+bottomright)),
4479 graph._connect(*(bottomright+topright)),
4480 graph._connect(*(topright+topleft)),
4481 path.closepath()))
4483 def drawpoints(self, graph, points):
4484 if self.colorindex is None:
4485 raise RuntimeError("column 'color' not set")
4486 self.lastcolor = None
4487 self.rectclipcanvas = graph.clipcanvas()
4488 symbol.drawpoints(self, graph, points)
4490 def key(self, c, x, y, width, height):
4491 raise RuntimeError("style doesn't yet provide a key")
4494 class text(symbol):
4496 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=textmodule.halign.center, **args):
4497 self.textindex = None
4498 self.textdx_str = textdx
4499 self.textdy_str = textdy
4500 self._textattrs = textattrs
4501 symbol.__init__(self, **args)
4503 def iteratedict(self):
4504 result = symbol.iteratedict()
4505 result["textattrs"] = _iterateattr(self._textattrs)
4506 return result
4508 def iterate(self):
4509 return textsymbol(**self.iteratedict())
4511 def othercolumnkey(self, key, index):
4512 if key == "text":
4513 self.textindex = index
4514 else:
4515 symbol.othercolumnkey(self, key, index)
4517 def drawsymbol_pt(self, graph, x, y, point=None):
4518 symbol.drawsymbol_pt(self, graph, x, y, point)
4519 if None not in (x, y, point[self.textindex]) and self._textattrs is not None:
4520 graph.text_pt(x + self.textdx_pt, y + self.textdy_pt, str(point[self.textindex]), *helper.ensuresequence(self.textattrs))
4522 def drawpoints(self, graph, points):
4523 self.textdx = unit.length(_getattr(self.textdx_str), default_type="v")
4524 self.textdy = unit.length(_getattr(self.textdy_str), default_type="v")
4525 self.textdx_pt = unit.topt(self.textdx)
4526 self.textdy_pt = unit.topt(self.textdy)
4527 if self._textattrs is not None:
4528 self.textattrs = _getattr(self._textattrs)
4529 if self.textindex is None:
4530 raise RuntimeError("column 'text' not set")
4531 symbol.drawpoints(self, graph, points)
4533 def key(self, c, x, y, width, height):
4534 raise RuntimeError("style doesn't yet provide a key")
4537 class arrow(symbol):
4539 def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
4540 self.linelength_str = linelength
4541 self.arrowsize_str = arrowsize
4542 self.arrowattrs = arrowattrs
4543 self.arrowdict = arrowdict
4544 self.epsilon = epsilon
4545 self.sizeindex = self.angleindex = None
4546 symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
4548 def iterate(self):
4549 raise RuntimeError("style is not iterateable")
4551 def othercolumnkey(self, key, index):
4552 if key == "size":
4553 self.sizeindex = index
4554 elif key == "angle":
4555 self.angleindex = index
4556 else:
4557 symbol.othercolumnkey(self, key, index)
4559 def drawsymbol_pt(self, graph, x, y, point=None):
4560 if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
4561 if point[self.sizeindex] > self.epsilon:
4562 dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
4563 x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
4564 y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
4565 x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
4566 y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
4567 graph.stroke(path.line(x1, y1, x2, y2),
4568 deco.earrow(self.arrowsize*point[self.sizeindex],
4569 **self.arrowdict),
4570 *helper.ensuresequence(self.arrowattrs))
4572 def drawpoints(self, graph, points):
4573 self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
4574 self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
4575 self.arrowsize_pt = unit.topt(self.arrowsize)
4576 self.linelength_pt = unit.topt(self.linelength)
4577 if self.sizeindex is None:
4578 raise RuntimeError("column 'size' not set")
4579 if self.angleindex is None:
4580 raise RuntimeError("column 'angle' not set")
4581 symbol.drawpoints(self, graph, points)
4583 def key(self, c, x, y, width, height):
4584 raise RuntimeError("style doesn't yet provide a key")
4587 class _bariterator(changeattr):
4589 def attr(self, index):
4590 return index, self.counter
4593 class bar:
4595 def __init__(self, fromzero=1, stacked=0, skipmissing=1, xbar=0,
4596 barattrs=helper.nodefault, _usebariterator=helper.nodefault, _previousbar=None):
4597 self.fromzero = fromzero
4598 self.stacked = stacked
4599 self.skipmissing = skipmissing
4600 self.xbar = xbar
4601 if barattrs is helper.nodefault:
4602 self._barattrs = (deco.stroked(color.gray.black), changecolor.Rainbow())
4603 else:
4604 self._barattrs = barattrs
4605 if _usebariterator is helper.nodefault:
4606 self.bariterator = _bariterator()
4607 else:
4608 self.bariterator = _usebariterator
4609 self.previousbar = _previousbar
4611 def iteratedict(self):
4612 result = {}
4613 result["barattrs"] = _iterateattrs(self._barattrs)
4614 return result
4616 def iterate(self):
4617 return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
4618 _usebariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
4620 def setcolumns(self, graph, columns):
4621 def checkpattern(key, index, pattern, iskey, isindex):
4622 if key is not None:
4623 match = pattern.match(key)
4624 if match:
4625 if isindex is not None: raise ValueError("multiple key specification")
4626 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4627 key = None
4628 iskey = match.groups()[0]
4629 isindex = index
4630 return key, iskey, isindex
4632 xkey = ykey = None
4633 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4634 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4635 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4636 xi = yi = None
4637 for key, index in columns.items():
4638 key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
4639 key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
4640 if key is not None:
4641 self.othercolumnkey(key, index)
4642 if None in (xkey, ykey): raise ValueError("incomplete axis specification")
4643 if self.xbar:
4644 self.nkey, self.ni = ykey, yi
4645 self.vkey, self.vi = xkey, xi
4646 else:
4647 self.nkey, self.ni = xkey, xi
4648 self.vkey, self.vi = ykey, yi
4649 self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
4651 def getranges(self, points):
4652 index, count = _getattr(self.bariterator)
4653 if count != 1 and self.stacked != 1:
4654 if self.stacked > 1:
4655 index = divmod(index, self.stacked)[0]
4657 vmin = vmax = None
4658 for point in points:
4659 if not self.skipmissing:
4660 if count != 1 and self.stacked != 1:
4661 self.naxis.setname(point[self.ni], index)
4662 else:
4663 self.naxis.setname(point[self.ni])
4664 try:
4665 v = point[self.vi] + 0.0
4666 if vmin is None or v < vmin: vmin = v
4667 if vmax is None or v > vmax: vmax = v
4668 except (TypeError, ValueError):
4669 pass
4670 else:
4671 if self.skipmissing:
4672 if count != 1 and self.stacked != 1:
4673 self.naxis.setname(point[self.ni], index)
4674 else:
4675 self.naxis.setname(point[self.ni])
4676 if self.fromzero:
4677 if vmin > 0: vmin = 0
4678 if vmax < 0: vmax = 0
4679 return {self.vkey: (vmin, vmax)}
4681 def drawpoints(self, graph, points):
4682 index, count = _getattr(self.bariterator)
4683 dostacked = (self.stacked != 0 and
4684 (self.stacked == 1 or divmod(index, self.stacked)[1]) and
4685 (self.stacked != 1 or index))
4686 if self.stacked > 1:
4687 index = divmod(index, self.stacked)[0]
4688 vmin, vmax = self.vaxis.getrange()
4689 self.barattrs = _getattrs(helper.ensuresequence(self._barattrs))
4690 if self.stacked:
4691 self.stackedvalue = {}
4692 for point in points:
4693 try:
4694 n = point[self.ni]
4695 v = point[self.vi]
4696 if self.stacked:
4697 self.stackedvalue[n] = v
4698 if count != 1 and self.stacked != 1:
4699 minid = (n, index, 0)
4700 maxid = (n, index, 1)
4701 else:
4702 minid = (n, 0)
4703 maxid = (n, 1)
4704 if self.xbar:
4705 x1pos, y1pos = graph.pos_pt(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
4706 x2pos, y2pos = graph.pos_pt(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4707 else:
4708 x1pos, y1pos = graph.pos_pt(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
4709 x2pos, y2pos = graph.pos_pt(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
4710 if dostacked:
4711 if self.xbar:
4712 x3pos, y3pos = graph.pos_pt(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
4713 x4pos, y4pos = graph.pos_pt(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
4714 else:
4715 x3pos, y3pos = graph.pos_pt(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4716 x4pos, y4pos = graph.pos_pt(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4717 else:
4718 if self.fromzero:
4719 if self.xbar:
4720 x3pos, y3pos = graph.pos_pt(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4721 x4pos, y4pos = graph.pos_pt(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
4722 else:
4723 x3pos, y3pos = graph.pos_pt(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4724 x4pos, y4pos = graph.pos_pt(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4725 else:
4726 #x3pos, y3pos = graph.tickpoint_pt(maxid, axis=self.naxis)
4727 #x4pos, y4pos = graph.tickpoint_pt(minid, axis=self.naxis)
4728 x3pos, y3pos = graph.axespos[self.nkey].tickpoint_pt(maxid)
4729 x4pos, y4pos = graph.axespos[self.nkey].tickpoint_pt(minid)
4730 if self.barattrs is not None:
4731 graph.fill(path.path(path._moveto(x1pos, y1pos),
4732 graph._connect(x1pos, y1pos, x2pos, y2pos),
4733 graph._connect(x2pos, y2pos, x3pos, y3pos),
4734 graph._connect(x3pos, y3pos, x4pos, y4pos),
4735 graph._connect(x4pos, y4pos, x1pos, y1pos), # no closepath (might not be straight)
4736 path.closepath()), *self.barattrs)
4737 except (TypeError, ValueError): pass
4739 def key(self, c, x, y, width, height):
4740 c.fill(path._rect(x, y, width, height), *self.barattrs)
4743 #class surface:
4745 # def setcolumns(self, graph, columns):
4746 # self.columns = columns
4748 # def getranges(self, points):
4749 # return {"x": (0, 10), "y": (0, 10), "z": (0, 1)}
4751 # def drawpoints(self, graph, points):
4752 # pass
4756 ################################################################################
4757 # data
4758 ################################################################################
4761 class data:
4763 defaultstyle = symbol
4765 def __init__(self, file, title=helper.nodefault, context={}, **columns):
4766 self.title = title
4767 if helper.isstring(file):
4768 self.data = datamodule.datafile(file)
4769 else:
4770 self.data = file
4771 if title is helper.nodefault:
4772 self.title = "(unknown)"
4773 else:
4774 self.title = title
4775 self.columns = {}
4776 for key, column in columns.items():
4777 try:
4778 self.columns[key] = self.data.getcolumnno(column)
4779 except datamodule.ColumnError:
4780 self.columns[key] = len(self.data.titles)
4781 self.data.addcolumn(column, context=context)
4783 def setstyle(self, graph, style):
4784 self.style = style
4785 self.style.setcolumns(graph, self.columns)
4787 def getranges(self):
4788 return self.style.getranges(self.data.data)
4790 def setranges(self, ranges):
4791 pass
4793 def draw(self, graph):
4794 self.style.drawpoints(graph, self.data.data)
4797 class function:
4799 defaultstyle = line
4801 def __init__(self, expression, title=helper.nodefault, min=None, max=None, points=100, parser=mathtree.parser(), context={}):
4802 if title is helper.nodefault:
4803 self.title = expression
4804 else:
4805 self.title = title
4806 self.min = min
4807 self.max = max
4808 self.points = points
4809 self.context = context
4810 self.result, expression = [x.strip() for x in expression.split("=")]
4811 self.mathtree = parser.parse(expression)
4812 self.variable = None
4813 self.evalranges = 0
4815 def setstyle(self, graph, style):
4816 for variable in self.mathtree.VarList():
4817 if variable in graph.axes.keys():
4818 if self.variable is None:
4819 self.variable = variable
4820 else:
4821 raise ValueError("multiple variables found")
4822 if self.variable is None:
4823 raise ValueError("no variable found")
4824 self.xaxis = graph.axes[self.variable]
4825 self.style = style
4826 self.style.setcolumns(graph, {self.variable: 0, self.result: 1})
4828 def getranges(self):
4829 if self.evalranges:
4830 return self.style.getranges(self.data)
4831 if None not in (self.min, self.max):
4832 return {self.variable: (self.min, self.max)}
4834 def setranges(self, ranges):
4835 if ranges.has_key(self.variable):
4836 min, max = ranges[self.variable]
4837 if self.min is not None: min = self.min
4838 if self.max is not None: max = self.max
4839 vmin = self.xaxis.convert(min)
4840 vmax = self.xaxis.convert(max)
4841 self.data = []
4842 for i in range(self.points):
4843 self.context[self.variable] = x = self.xaxis.invert(vmin + (vmax-vmin)*i / (self.points-1.0))
4844 try:
4845 y = self.mathtree.Calc(**self.context)
4846 except (ArithmeticError, ValueError):
4847 y = None
4848 self.data.append((x, y))
4849 self.evalranges = 1
4851 def draw(self, graph):
4852 self.style.drawpoints(graph, self.data)
4855 class paramfunction:
4857 defaultstyle = line
4859 def __init__(self, varname, min, max, expression, title=helper.nodefault, points=100, parser=mathtree.parser(), context={}):
4860 if title is helper.nodefault:
4861 self.title = expression
4862 else:
4863 self.title = title
4864 self.varname = varname
4865 self.min = min
4866 self.max = max
4867 self.points = points
4868 self.expression = {}
4869 self.mathtrees = {}
4870 varlist, expressionlist = expression.split("=")
4871 parsestr = mathtree.ParseStr(expressionlist)
4872 for key in varlist.split(","):
4873 key = key.strip()
4874 if self.mathtrees.has_key(key):
4875 raise ValueError("multiple assignment in tuple")
4876 try:
4877 self.mathtrees[key] = parser.ParseMathTree(parsestr)
4878 break
4879 except mathtree.CommaFoundMathTreeParseError, e:
4880 self.mathtrees[key] = e.MathTree
4881 else:
4882 raise ValueError("unpack tuple of wrong size")
4883 if len(varlist.split(",")) != len(self.mathtrees.keys()):
4884 raise ValueError("unpack tuple of wrong size")
4885 self.data = []
4886 for i in range(self.points):
4887 context[self.varname] = self.min + (self.max-self.min)*i / (self.points-1.0)
4888 line = []
4889 for key, tree in self.mathtrees.items():
4890 line.append(tree.Calc(**context))
4891 self.data.append(line)
4893 def setstyle(self, graph, style):
4894 self.style = style
4895 columns = {}
4896 for key, index in zip(self.mathtrees.keys(), xrange(sys.maxint)):
4897 columns[key] = index
4898 self.style.setcolumns(graph, columns)
4900 def getranges(self):
4901 return self.style.getranges(self.data)
4903 def setranges(self, ranges):
4904 pass
4906 def draw(self, graph):
4907 self.style.drawpoints(graph, self.data)