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