some code reorganisation/renaming: separate graph directory, write(-Postscript) metho...
[PyX/mjg.git] / pyx / graph.py
bloba867514ef1ea75ab98c84743209fab9e44d8e199
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_pt = self.normpath.arclength_pt()
1541 self.arclength = unit.t_pt(self.arclength_pt)
1542 _axispos.__init__(self, convert)
1543 self.direction = direction
1545 def vbasepath(self, v1=None, v2=None):
1546 if v1 is None:
1547 if v2 is None:
1548 return self.path
1549 else:
1550 return self.normpath.split(self.normpath.lentopar(v2 * self.arclength))[0]
1551 else:
1552 if v2 is None:
1553 return self.normpath.split(self.normpath.lentopar(v1 * self.arclength))[1]
1554 else:
1555 return self.normpath.split(*self.normpath.lentopar([v1 * self.arclength, v2 * self.arclength]))[1]
1557 def vgridpath(self, v):
1558 return None
1560 def vtickpoint_pt(self, v):
1561 return self.normpath.at_pt(self.normpath.lentopar(v * self.arclength))
1563 def vtickdirection(self, v):
1564 t = self.normpath.tangent(self.normpath.lentopar(v * self.arclength))
1565 tbegin = t.begin_pt()
1566 tend = t.end_pt()
1567 dx = tend[0]-tbegin[0]
1568 dy = tend[1]-tbegin[1]
1569 norm = math.sqrt(dx*dx + dy*dy)
1570 if self.direction == 1:
1571 return -dy/norm, dx/norm
1572 elif self.direction == -1:
1573 return dy/norm, -dx/norm
1574 raise RuntimeError("unknown direction")
1577 class axistitlepainter:
1578 """class for painting an axis title
1579 - the axis must have a title attribute when using this painter;
1580 this title might be None"""
1582 __implements__ = _Iaxispainter
1584 defaulttitleattrs = [textmodule.halign.center, textmodule.vshift.mathaxis]
1586 def __init__(self, titledist="0.3 cm",
1587 titleattrs=[],
1588 titledirection=rotatetext.parallel,
1589 titlepos=0.5,
1590 texrunner=textmodule.defaulttexrunner):
1591 """initialized the instance
1592 - titledist is a visual PyX length giving the distance
1593 of the title from the axis extent already there (a title might
1594 be added after labels or other things are plotted already)
1595 - titleattrs is a list of attributes for a texrunners text
1596 method; a single is allowed without being a list; None
1597 turns off the title
1598 - titledirection is an instance of rotatetext or None
1599 - titlepos is the position of the title in graph coordinates
1600 - texrunner is the texrunner to be used to create text
1601 (the texrunner is available for further use in derived
1602 classes as instance variable texrunner)"""
1603 self.titledist_str = titledist
1604 self.titleattrs = titleattrs
1605 self.titledirection = titledirection
1606 self.titlepos = titlepos
1607 self.texrunner = texrunner
1609 def paint(self, axispos, axis, ac=None):
1610 if ac is None:
1611 ac = axiscanvas()
1612 if axis.title is not None and self.titleattrs is not None:
1613 titledist = unit.length(self.titledist_str, default_type="v")
1614 x, y = axispos.vtickpoint_pt(self.titlepos)
1615 dx, dy = axispos.vtickdirection(self.titlepos)
1616 titleattrs = self.defaulttitleattrs + self.titleattrs
1617 if self.titledirection is not None:
1618 titleattrs.append(self.titledirection.trafo(dx, dy))
1619 title = self.texrunner.text_pt(x, y, axis.title, titleattrs)
1620 ac.extent += titledist
1621 title.linealign(ac.extent, -dx, -dy)
1622 ac.extent += title.extent(dx, dy)
1623 ac.insert(title)
1624 return ac
1627 class geometricseries(attr.changeattr):
1629 def __init__(self, initial, factor):
1630 self.initial = initial
1631 self.factor = factor
1633 def select(self, index, total):
1634 return self.initial * (self.factor ** index)
1637 class ticklength(geometricseries): pass
1639 _base = 0.2
1641 #ticklength.short = ticklength("%f cm" % (_base/math.sqrt(64)), 1/goldenmean)
1642 ticklength.short = ticklength(_base/math.sqrt(64), 1/goldenmean)
1643 ticklength.short = ticklength(_base/math.sqrt(32), 1/goldenmean)
1644 ticklength.short = ticklength(_base/math.sqrt(16), 1/goldenmean)
1645 ticklength.short = ticklength(_base/math.sqrt(8), 1/goldenmean)
1646 ticklength.short = ticklength(_base/math.sqrt(4), 1/goldenmean)
1647 ticklength.short = ticklength(_base/math.sqrt(2), 1/goldenmean)
1648 ticklength.normal = ticklength(_base, 1/goldenmean)
1649 ticklength.long = ticklength(_base*math.sqrt(2), 1/goldenmean)
1650 ticklength.long = ticklength(_base*math.sqrt(4), 1/goldenmean)
1651 ticklength.long = ticklength(_base*math.sqrt(8), 1/goldenmean)
1652 ticklength.long = ticklength(_base*math.sqrt(16), 1/goldenmean)
1653 ticklength.long = ticklength(_base*math.sqrt(32), 1/goldenmean)
1656 class axispainter(axistitlepainter):
1657 """class for painting the ticks and labels of an axis
1658 - the inherited titleaxispainter is used to paint the title of
1659 the axis
1660 - note that the type of the elements of ticks given as an argument
1661 of the paint method must be suitable for the tick position methods
1662 of the axis"""
1664 __implements__ = _Iaxispainter
1666 defaulttickattrs = []
1667 defaultgridattrs = []
1668 defaultbasepathattrs = [style.linecap.square]
1669 defaultlabelattrs = [textmodule.halign.center, textmodule.vshift.mathaxis]
1671 def __init__(self, innerticklength=ticklength.short,
1672 outerticklength=None,
1673 tickattrs=[],
1674 gridattrs=None,
1675 basepathattrs=[],
1676 labeldist="0.3 cm",
1677 labelattrs=[],
1678 labeldirection=None,
1679 labelhequalize=0,
1680 labelvequalize=1,
1681 **kwargs):
1682 """initializes the instance
1683 - innerticklength and outerticklength are two lists of
1684 visual PyX lengths for ticks, subticks, etc. plotted inside
1685 and outside of the graph; when a single value is given, it
1686 is used for all tick levels; None turns off ticks inside or
1687 outside of the graph
1688 - tickattrs are a list of stroke attributes for the ticks;
1689 a single entry is allowed without being a list; None turns
1690 off ticks
1691 - gridattrs are a list of lists used as stroke
1692 attributes for ticks, subticks etc.; when a single list
1693 is given, it is used for ticks, subticks, etc.; a single
1694 entry is allowed without being a list; None turns off
1695 the grid
1696 - basepathattrs are a list of stroke attributes for a grid
1697 line at axis value zero; a single entry is allowed without
1698 being a list; None turns off the basepath
1699 - labeldist is a visual PyX length for the distance of the labels
1700 from the axis basepath
1701 - labelattrs is a list of attributes for a texrunners text
1702 method; a single entry is allowed without being a list;
1703 None turns off the labels
1704 - titledirection is an instance of rotatetext or None
1705 - labelhequalize and labelvequalize (booleans) perform an equal
1706 alignment for straight vertical and horizontal axes, respectively
1707 - futher keyword arguments are passed to axistitlepainter"""
1708 # TODO: access to axis.divisor -- document, remove, ... ???
1709 self.innerticklength_str = innerticklength
1710 self.outerticklength_str = outerticklength
1711 self.tickattrs = tickattrs
1712 self.gridattrs = gridattrs
1713 self.basepathattrs = basepathattrs
1714 self.labeldist_str = labeldist
1715 self.labelattrs = labelattrs
1716 self.labeldirection = labeldirection
1717 self.labelhequalize = labelhequalize
1718 self.labelvequalize = labelvequalize
1719 axistitlepainter.__init__(self, **kwargs)
1721 def paint(self, axispos, axis, ac=None):
1722 if ac is None:
1723 ac = axiscanvas()
1724 labeldist = unit.length(self.labeldist_str, default_type="v")
1725 for tick in axis.ticks:
1726 tick.temp_v = axis.convert(float(tick) * axis.divisor)
1727 tick.temp_x, tick.temp_y = axispos.vtickpoint_pt(tick.temp_v)
1728 tick.temp_dx, tick.temp_dy = axispos.vtickdirection(tick.temp_v)
1729 maxticklevel, maxlabellevel = _maxlevels(axis.ticks)
1731 # create & align tick.temp_labelbox
1732 for tick in axis.ticks:
1733 if tick.labellevel is not None:
1734 labelattrs = attr.selectattrs(self.labelattrs, tick.labellevel, maxlabellevel)
1735 if labelattrs is not None:
1736 labelattrs = self.defaultlabelattrs + labelattrs
1737 if self.labeldirection is not None:
1738 labelattrs.append(self.labeldirection.trafo(tick.temp_dx, tick.temp_dy))
1739 if tick.labelattrs is not None:
1740 labelattrs.extend(tick.labelattrs)
1741 tick.temp_labelbox = self.texrunner.text_pt(tick.temp_x, tick.temp_y, tick.label, labelattrs)
1742 if len(axis.ticks) > 1:
1743 equaldirection = 1
1744 for tick in axis.ticks[1:]:
1745 if tick.temp_dx != axis.ticks[0].temp_dx or tick.temp_dy != axis.ticks[0].temp_dy:
1746 equaldirection = 0
1747 else:
1748 equaldirection = 0
1749 if equaldirection and ((not axis.ticks[0].temp_dx and self.labelvequalize) or
1750 (not axis.ticks[0].temp_dy and self.labelhequalize)):
1751 if self.labelattrs is not None:
1752 box.linealignequal([tick.temp_labelbox for tick in axis.ticks if tick.labellevel is not None],
1753 labeldist, -axis.ticks[0].temp_dx, -axis.ticks[0].temp_dy)
1754 else:
1755 for tick in axis.ticks:
1756 if tick.labellevel is not None and self.labelattrs is not None:
1757 tick.temp_labelbox.linealign(labeldist, -tick.temp_dx, -tick.temp_dy)
1759 for tick in axis.ticks:
1760 if tick.ticklevel is not None:
1761 innerticklength = attr.selectattr(self.innerticklength_str, tick.ticklevel, maxticklevel)
1762 outerticklength = attr.selectattr(self.outerticklength_str, tick.ticklevel, maxticklevel)
1763 if innerticklength is not None or outerticklength is not None:
1764 if innerticklength is None:
1765 innerticklength = 0
1766 else:
1767 innerticklength = unit.length(innerticklength, default_type="v")
1768 if outerticklength is None:
1769 outerticklength = 0
1770 else:
1771 outerticklength = unit.length(outerticklength, default_type="v")
1772 tickattrs = attr.selectattrs(self.defaulttickattrs + self.tickattrs, tick.ticklevel, maxticklevel)
1773 if tickattrs is not None:
1774 innerticklength_pt = unit.topt(innerticklength)
1775 outerticklength_pt = unit.topt(outerticklength)
1776 x1 = tick.temp_x + tick.temp_dx * innerticklength_pt
1777 y1 = tick.temp_y + tick.temp_dy * innerticklength_pt
1778 x2 = tick.temp_x - tick.temp_dx * outerticklength_pt
1779 y2 = tick.temp_y - tick.temp_dy * outerticklength_pt
1780 ac.stroke(path.line_pt(x1, y1, x2, y2), tickattrs)
1781 if outerticklength is not None and unit.topt(outerticklength) > unit.topt(ac.extent):
1782 ac.extent = outerticklength
1783 if outerticklength is not None and unit.topt(-innerticklength) > unit.topt(ac.extent):
1784 ac.extent = -innerticklength
1785 if self.gridattrs is not None:
1786 gridattrs = attr.selectattrs(self.defaultgridattrs + self.gridattrs, tick.ticklevel, maxticklevel)
1787 ac.stroke(axispos.vgridpath(tick.temp_v), gridattrs)
1788 if tick.labellevel is not None and self.labelattrs is not None:
1789 ac.insert(tick.temp_labelbox)
1790 ac.labels.append(tick.temp_labelbox)
1791 extent = tick.temp_labelbox.extent(tick.temp_dx, tick.temp_dy) + labeldist
1792 if unit.topt(extent) > unit.topt(ac.extent):
1793 ac.extent = extent
1794 if self.basepathattrs is not None:
1795 ac.stroke(axispos.vbasepath(), self.defaultbasepathattrs + self.basepathattrs)
1797 # for tick in axis.ticks:
1798 # del tick.temp_v # we've inserted those temporary variables ... and do not care any longer about them
1799 # del tick.temp_x
1800 # del tick.temp_y
1801 # del tick.temp_dx
1802 # del tick.temp_dy
1803 # if tick.labellevel is not None and self.labelattrs is not None:
1804 # del tick.temp_labelbox
1806 axistitlepainter.paint(self, axispos, axis, ac=ac)
1808 return ac
1811 class linkaxispainter(axispainter):
1812 """class for painting a linked axis
1813 - the inherited axispainter is used to paint the axis
1814 - modifies some constructor defaults"""
1816 __implements__ = _Iaxispainter
1818 def __init__(self, labelattrs=None,
1819 titleattrs=None,
1820 **kwargs):
1821 """initializes the instance
1822 - the labelattrs default is set to None thus skipping the labels
1823 - the titleattrs default is set to None thus skipping the title
1824 - all keyword arguments are passed to axispainter"""
1825 axispainter.__init__(self, labelattrs=labelattrs,
1826 titleattrs=titleattrs,
1827 **kwargs)
1830 class subaxispos:
1831 """implementation of the _Iaxispos interface for a subaxis"""
1833 __implements__ = _Iaxispos
1835 def __init__(self, convert, baseaxispos, vmin, vmax, vminover, vmaxover):
1836 """initializes the instance
1837 - convert is the subaxis convert method
1838 - baseaxispos is the axispos instance of the base axis
1839 - vmin, vmax is the range covered by the subaxis in graph coordinates
1840 - vminover, vmaxover is the extended range of the subaxis including
1841 regions between several subaxes (for basepath drawing etc.)"""
1842 self.convert = convert
1843 self.baseaxispos = baseaxispos
1844 self.vmin = vmin
1845 self.vmax = vmax
1846 self.vminover = vminover
1847 self.vmaxover = vmaxover
1849 def basepath(self, x1=None, x2=None):
1850 if x1 is not None:
1851 v1 = self.vmin+self.convert(x1)*(self.vmax-self.vmin)
1852 else:
1853 v1 = self.vminover
1854 if x2 is not None:
1855 v2 = self.vmin+self.convert(x2)*(self.vmax-self.vmin)
1856 else:
1857 v2 = self.vmaxover
1858 return self.baseaxispos.vbasepath(v1, v2)
1860 def vbasepath(self, v1=None, v2=None):
1861 if v1 is not None:
1862 v1 = self.vmin+v1*(self.vmax-self.vmin)
1863 else:
1864 v1 = self.vminover
1865 if v2 is not None:
1866 v2 = self.vmin+v2*(self.vmax-self.vmin)
1867 else:
1868 v2 = self.vmaxover
1869 return self.baseaxispos.vbasepath(v1, v2)
1871 def gridpath(self, x):
1872 return self.baseaxispos.vgridpath(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1874 def vgridpath(self, v):
1875 return self.baseaxispos.vgridpath(self.vmin+v*(self.vmax-self.vmin))
1877 def tickpoint_pt(self, x, axis=None):
1878 return self.baseaxispos.vtickpoint_pt(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1880 def tickpoint(self, x, axis=None):
1881 return self.baseaxispos.vtickpoint(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1883 def vtickpoint_pt(self, v, axis=None):
1884 return self.baseaxispos.vtickpoint_pt(self.vmin+v*(self.vmax-self.vmin))
1886 def vtickpoint(self, v, axis=None):
1887 return self.baseaxispos.vtickpoint(self.vmin+v*(self.vmax-self.vmin))
1889 def tickdirection(self, x, axis=None):
1890 return self.baseaxispos.vtickdirection(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1892 def vtickdirection(self, v, axis=None):
1893 return self.baseaxispos.vtickdirection(self.vmin+v*(self.vmax-self.vmin))
1896 class splitaxispainter(axistitlepainter):
1897 """class for painting a splitaxis
1898 - the inherited titleaxispainter is used to paint the title of
1899 the axis
1900 - the splitaxispainter access the subaxes attribute of the axis"""
1902 __implements__ = _Iaxispainter
1904 defaultbreaklinesattrs = []
1906 def __init__(self, breaklinesdist="0.05 cm",
1907 breaklineslength="0.5 cm",
1908 breaklinesangle=-60,
1909 breaklinesattrs=[],
1910 **args):
1911 """initializes the instance
1912 - breaklinesdist is a visual length of the distance between
1913 the two lines of the axis break
1914 - breaklineslength is a visual length of the length of the
1915 two lines of the axis break
1916 - breaklinesangle is the angle of the lines of the axis break
1917 - breaklinesattrs are a list of stroke attributes for the
1918 axis break lines; a single entry is allowed without being a
1919 list; None turns off the break lines
1920 - futher keyword arguments are passed to axistitlepainter"""
1921 self.breaklinesdist_str = breaklinesdist
1922 self.breaklineslength_str = breaklineslength
1923 self.breaklinesangle = breaklinesangle
1924 self.breaklinesattrs = breaklinesattrs
1925 axistitlepainter.__init__(self, **args)
1927 def paint(self, axispos, axis, ac=None):
1928 if ac is None:
1929 ac = axiscanvas()
1930 for subaxis in axis.subaxes:
1931 subaxis.finish(subaxispos(subaxis.convert, axispos, subaxis.vmin, subaxis.vmax, subaxis.vminover, subaxis.vmaxover))
1932 ac.insert(subaxis.axiscanvas)
1933 if unit.topt(ac.extent) < unit.topt(subaxis.axiscanvas.extent):
1934 ac.extent = subaxis.axiscanvas.extent
1935 if self.breaklinesattrs is not None:
1936 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1937 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1938 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1939 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1940 breaklinesextent = (0.5*self.breaklinesdist*math.fabs(self.cos) +
1941 0.5*self.breaklineslength*math.fabs(self.sin))
1942 if unit.topt(ac.extent) < unit.topt(breaklinesextent):
1943 ac.extent = breaklinesextent
1944 for subaxis1, subaxis2 in zip(axis.subaxes[:-1], axis.subaxes[1:]):
1945 # use a tangent of the basepath (this is independent of the tickdirection)
1946 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1947 p = path.normpath(axispos.vbasepath(v, None))
1948 breakline = p.tangent(0, self.breaklineslength)
1949 widthline = p.tangent(0, self.breaklinesdist).transformed(trafomodule.rotate(self.breaklinesangle+90, *breakline.begin()))
1950 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1951 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1952 breakline = breakline.transformed(trafomodule.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1953 breakline1 = breakline.transformed(trafomodule.translate(*towidth))
1954 breakline2 = breakline.transformed(trafomodule.translate(-towidth[0], -towidth[1]))
1955 ac.fill(path.path(path.moveto_pt(*breakline1.begin_pt()),
1956 path.lineto_pt(*breakline1.end_pt()),
1957 path.lineto_pt(*breakline2.end_pt()),
1958 path.lineto_pt(*breakline2.begin_pt()),
1959 path.closepath()), [color.gray.white])
1960 ac.stroke(breakline1, self.defaultbreaklinesattrs + self.breaklinesattrs)
1961 ac.stroke(breakline2, self.defaultbreaklinesattrs + self.breaklinesattrs)
1962 axistitlepainter.paint(self, axispos, axis, ac=ac)
1963 return ac
1966 class linksplitaxispainter(splitaxispainter):
1967 """class for painting a linked splitaxis
1968 - the inherited splitaxispainter is used to paint the axis
1969 - modifies some constructor defaults"""
1971 __implements__ = _Iaxispainter
1973 def __init__(self, titleattrs=None, **kwargs):
1974 """initializes the instance
1975 - the titleattrs default is set to None thus skipping the title
1976 - all keyword arguments are passed to splitaxispainter"""
1977 splitaxispainter.__init__(self, titleattrs=titleattrs, **kwargs)
1980 class baraxispainter(axistitlepainter):
1981 """class for painting a baraxis
1982 - the inherited titleaxispainter is used to paint the title of
1983 the axis
1984 - the baraxispainter access the multisubaxis, subaxis names, texts, and
1985 relsizes attributes"""
1987 __implements__ = _Iaxispainter
1989 defaulttickattrs = []
1990 defaultbasepathattrs = [style.linecap.square]
1991 defaultnameattrs = [textmodule.halign.center, textmodule.vshift.mathaxis]
1993 def __init__(self, innerticklength=None,
1994 outerticklength=None,
1995 tickattrs=[],
1996 basepathattrs=[],
1997 namedist="0.3 cm",
1998 nameattrs=[],
1999 namedirection=None,
2000 namepos=0.5,
2001 namehequalize=0,
2002 namevequalize=1,
2003 **args):
2004 """initializes the instance
2005 - innerticklength and outerticklength are a visual length of
2006 the ticks to be plotted at the axis basepath to visually
2007 separate the bars; if neither innerticklength nor
2008 outerticklength are set, not ticks are plotted
2009 - breaklinesattrs are a list of stroke attributes for the
2010 axis tick; a single entry is allowed without being a
2011 list; None turns off the ticks
2012 - namedist is a visual PyX length for the distance of the bar
2013 names from the axis basepath
2014 - nameattrs is a list of attributes for a texrunners text
2015 method; a single entry is allowed without being a list;
2016 None turns off the names
2017 - namedirection is an instance of rotatetext or None
2018 - namehequalize and namevequalize (booleans) perform an equal
2019 alignment for straight vertical and horizontal axes, respectively
2020 - futher keyword arguments are passed to axistitlepainter"""
2021 self.innerticklength_str = innerticklength
2022 self.outerticklength_str = outerticklength
2023 self.tickattrs = tickattrs
2024 self.basepathattrs = basepathattrs
2025 self.namedist_str = namedist
2026 self.nameattrs = nameattrs
2027 self.namedirection = namedirection
2028 self.namepos = namepos
2029 self.namehequalize = namehequalize
2030 self.namevequalize = namevequalize
2031 axistitlepainter.__init__(self, **args)
2033 def paint(self, axispos, axis, ac=None):
2034 if ac is None:
2035 ac = axiscanvas()
2036 if axis.multisubaxis is not None:
2037 for subaxis in axis.subaxis:
2038 subaxis.finish(subaxispos(subaxis.convert, axispos, subaxis.vmin, subaxis.vmax, None, None))
2039 ac.insert(subaxis.axiscanvas)
2040 if unit.topt(ac.extent) < unit.topt(subaxis.axiscanvas.extent):
2041 ac.extent = subaxis.axiscanvas.extent
2042 namepos = []
2043 for name in axis.names:
2044 v = axis.convert((name, self.namepos))
2045 x, y = axispos.vtickpoint_pt(v)
2046 dx, dy = axispos.vtickdirection(v)
2047 namepos.append((v, x, y, dx, dy))
2048 nameboxes = []
2049 if self.nameattrs is not None:
2050 for (v, x, y, dx, dy), name in zip(namepos, axis.names):
2051 nameattrs = self.defaultnameattrs + self.nameattrs
2052 if self.namedirection is not None:
2053 nameattrs.append(self.namedirection.trafo(tick.temp_dx, tick.temp_dy))
2054 if axis.texts.has_key(name):
2055 nameboxes.append(self.texrunner.text_pt(x, y, str(axis.texts[name]), nameattrs))
2056 elif axis.texts.has_key(str(name)):
2057 nameboxes.append(self.texrunner.text_pt(x, y, str(axis.texts[str(name)]), nameattrs))
2058 else:
2059 nameboxes.append(self.texrunner.text_pt(x, y, str(name), nameattrs))
2060 labeldist = ac.extent + unit.length(self.namedist_str, default_type="v")
2061 if len(namepos) > 1:
2062 equaldirection = 1
2063 for np in namepos[1:]:
2064 if np[3] != namepos[0][3] or np[4] != namepos[0][4]:
2065 equaldirection = 0
2066 else:
2067 equaldirection = 0
2068 if equaldirection and ((not namepos[0][3] and self.namevequalize) or
2069 (not namepos[0][4] and self.namehequalize)):
2070 box.linealignequal(nameboxes, labeldist, -namepos[0][3], -namepos[0][4])
2071 else:
2072 for namebox, np in zip(nameboxes, namepos):
2073 namebox.linealign(labeldist, -np[3], -np[4])
2074 if self.basepathattrs is not None:
2075 p = axispos.vbasepath()
2076 if p is not None:
2077 ac.stroke(p, self.defaultbasepathattrs + self.basepathattrs)
2078 if self.tickattrs is not None and (self.innerticklength_str is not None or
2079 self.outerticklength_str is not None):
2080 if self.innerticklength_str is not None:
2081 innerticklength = unit.length(self.innerticklength_str, default_type="v")
2082 innerticklength_pt = unit.topt(innerticklength)
2083 if unit.topt(ac.extent) < -innerticklength_pt:
2084 ac.extent = -innerticklength
2085 elif self.outerticklength_str is not None:
2086 innerticklength = innerticklength_pt = 0
2087 if self.outerticklength_str is not None:
2088 outerticklength = unit.length(self.outerticklength_str, default_type="v")
2089 outerticklength_pt = unit.topt(outerticklength)
2090 if unit.topt(ac.extent) < outerticklength_pt:
2091 ac.extent = outerticklength
2092 elif self.innerticklength_str is not None:
2093 outerticklength = outerticklength_pt = 0
2094 for pos in axis.relsizes:
2095 if pos == axis.relsizes[0]:
2096 pos -= axis.firstdist
2097 elif pos != axis.relsizes[-1]:
2098 pos -= 0.5 * axis.dist
2099 v = pos / axis.relsizes[-1]
2100 x, y = axispos.vtickpoint_pt(v)
2101 dx, dy = axispos.vtickdirection(v)
2102 x1 = x + dx * innerticklength_pt
2103 y1 = y + dy * innerticklength_pt
2104 x2 = x - dx * outerticklength_pt
2105 y2 = y - dy * outerticklength_pt
2106 ac.stroke(path.line_pt(x1, y1, x2, y2), self.defaulttickattrs + self.tickattrs)
2107 for (v, x, y, dx, dy), namebox in zip(namepos, nameboxes):
2108 newextent = namebox.extent(dx, dy) + labeldist
2109 if unit.topt(ac.extent) < unit.topt(newextent):
2110 ac.extent = newextent
2111 for namebox in nameboxes:
2112 ac.insert(namebox)
2113 axistitlepainter.paint(self, axispos, axis, ac=ac)
2114 return ac
2117 class linkbaraxispainter(baraxispainter):
2118 """class for painting a linked baraxis
2119 - the inherited baraxispainter is used to paint the axis
2120 - modifies some constructor defaults"""
2122 __implements__ = _Iaxispainter
2124 def __init__(self, nameattrs=None, titleattrs=None, **kwargs):
2125 """initializes the instance
2126 - the titleattrs default is set to None thus skipping the title
2127 - the nameattrs default is set to None thus skipping the names
2128 - all keyword arguments are passed to axispainter"""
2129 baraxispainter.__init__(self, nameattrs=nameattrs, titleattrs=titleattrs, **kwargs)
2132 ################################################################################
2133 # axes
2134 ################################################################################
2137 class _Iaxis:
2138 """interface definition of a axis
2139 - an axis should implement an convert and invert method like
2140 _Imap, but this is not part of this interface definition;
2141 one possibility is to mix-in a proper map class, but special
2142 purpose axes might do something else
2143 - an axis has the instance variable axiscanvas after the finish
2144 method was called
2145 - an axis might have further instance variables (title, ticks)
2146 to be used in combination with appropriate axispainters"""
2148 def convert(self, x):
2149 "convert a value into graph coordinates"
2151 def invert(self, v):
2152 "invert a graph coordinate to a axis value"
2154 def getrelsize(self):
2155 """returns the relative size (width) of the axis
2156 - for use in splitaxis, baraxis etc.
2157 - might return None if no size is available"""
2159 # TODO: describe adjustrange
2160 def setrange(self, min=None, max=None):
2161 """set the axis data range
2162 - the type of min and max must fit to the axis
2163 - min<max; the axis might be reversed, but this is
2164 expressed internally only (min<max all the time)
2165 - the axis might not apply the change of the range
2166 (e.g. when the axis range is fixed by the user),
2167 but usually the range is extended to contain the
2168 given range
2169 - for invalid parameters (e.g. negativ values at an
2170 logarithmic axis), an exception should be raised
2171 - a RuntimeError is raised, when setrange is called
2172 after the finish method"""
2174 def getrange(self):
2175 """return data range as a tuple (min, max)
2176 - min<max; the axis might be reversed, but this is
2177 expressed internally only
2178 - a RuntimeError exception is raised when no
2179 range is available"""
2181 def finish(self, axispos):
2182 """finishes the axis
2183 - axispos implements _Iaxispos
2184 - sets the instance axiscanvas, which is insertable into the
2185 graph to finally paint the axis
2186 - any modification of the axis range should be disabled after
2187 the finish method was called"""
2188 # TODO: be more specific about exceptions
2190 def createlinkaxis(self, **kwargs):
2191 """create a link axis to the axis itself
2192 - typically, a link axis is a axis, which share almost
2193 all properties with the axis it is linked to
2194 - typically, the painter gets replaced by a painter
2195 which doesn't put any text to the axis"""
2198 class _axis:
2199 """base implementation a regular axis
2200 - typical usage is to mix-in a linmap or a logmap to
2201 complete the axis interface
2202 - note that some methods of this class want to access a
2203 parter and a rater; those attributes implementing _Iparter
2204 and _Irater should be initialized by the constructors
2205 of derived classes"""
2207 def __init__(self, min=None, max=None, reverse=0, divisor=1,
2208 title=None, painter=axispainter(), texter=defaulttexter(),
2209 density=1, maxworse=2, manualticks=[]):
2210 """initializes the instance
2211 - min and max fix the axis minimum and maximum, respectively;
2212 they are determined by the data to be plotted, when not fixed
2213 - reverse (boolean) reverses the minimum and the maximum of
2214 the axis
2215 - numerical divisor for the axis partitioning
2216 - title is a string containing the axis title
2217 - axispainter is the axis painter (should implement _Ipainter)
2218 - texter is the texter (should implement _Itexter)
2219 - density is a global parameter for the axis paritioning and
2220 axis rating; its default is 1, but the range 0.5 to 2.5 should
2221 be usefull to get less or more ticks by the automatic axis
2222 partitioning
2223 - maxworse is a number of trials with worse tick rating
2224 before giving up (usually it should not be needed to increase
2225 this value; increasing the number will slow down the automatic
2226 axis partitioning considerably)
2227 - manualticks and the partitioner results are mixed
2228 by _mergeticklists
2229 - note that some methods of this class want to access a
2230 parter and a rater; those attributes implementing _Iparter
2231 and _Irater should be initialized by the constructors
2232 of derived classes"""
2233 if min is not None and max is not None and min > max:
2234 min, max, reverse = max, min, not reverse
2235 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
2236 self.divisor = divisor
2237 self.title = title
2238 self.painter = painter
2239 self.texter = texter
2240 self.density = density
2241 self.maxworse = maxworse
2242 self.manualticks = self.checkfraclist(manualticks)
2243 self.canconvert = 0
2244 self.axiscanvas = None
2245 self._setrange()
2247 def _setrange(self, min=None, max=None):
2248 if not self.fixmin and min is not None and (self.min is None or min < self.min):
2249 self.min = min
2250 if not self.fixmax and max is not None and (self.max is None or max > self.max):
2251 self.max = max
2252 if None not in (self.min, self.max) and self.min != self.max:
2253 self.canconvert = 1
2254 if self.reverse:
2255 self.setbasepoints(((self.min, 1), (self.max, 0)))
2256 else:
2257 self.setbasepoints(((self.min, 0), (self.max, 1)))
2259 def _getrange(self):
2260 return self.min, self.max
2262 def _forcerange(self, range):
2263 self.min, self.max = range
2264 self._setrange()
2266 def setrange(self, min=None, max=None):
2267 oldmin, oldmax = self.min, self.max
2268 self._setrange(min, max)
2269 if self.axiscanvas is not None and ((oldmin != self.min) or (oldmax != self.max)):
2270 raise RuntimeError("range modification while axis was already finished")
2272 zero = 0.0
2274 def adjustrange(self, points, index, deltaindex=None, deltaminindex=None, deltamaxindex=None):
2275 min = max = None
2276 if len([x for x in [deltaindex, deltaminindex, deltamaxindex] if x is not None]) > 1:
2277 raise RuntimeError("only one of delta???index should set")
2278 if deltaindex is not None:
2279 deltaminindex = deltamaxindex = deltaindex
2280 if deltaminindex is not None:
2281 for point in points:
2282 try:
2283 value = point[index] - point[deltaminindex] + self.zero
2284 except:
2285 pass
2286 else:
2287 if min is None or value < min: min = value
2288 if max is None or value > max: max = value
2289 elif deltamaxindex is not None:
2290 for point in points:
2291 try:
2292 value = point[index] + point[deltamaxindex] + self.zero
2293 except:
2294 pass
2295 else:
2296 if min is None or value < min: min = value
2297 if max is None or value > max: max = value
2298 else:
2299 for point in points:
2300 try:
2301 value = point[index] + self.zero
2302 except:
2303 pass
2304 else:
2305 if min is None or value < min: min = value
2306 if max is None or value > max: max = value
2307 self.setrange(min, max)
2309 def getrange(self):
2310 if self.min is not None and self.max is not None:
2311 return self.min, self.max
2313 def checkfraclist(self, fracs):
2314 "orders a list of fracs, equal entries are not allowed"
2315 if not len(fracs): return []
2316 sorted = list(fracs)
2317 sorted.sort()
2318 last = sorted[0]
2319 for item in sorted[1:]:
2320 if last == item:
2321 raise ValueError("duplicate entry found")
2322 last = item
2323 return sorted
2325 def finish(self, axispos):
2326 if self.axiscanvas is not None: return
2328 # lesspart and morepart can be called after defaultpart;
2329 # this works although some axes may share their autoparting,
2330 # because the axes are processed sequentially
2331 first = 1
2332 if self.parter is not None:
2333 min, max = self.getrange()
2334 self.ticks = _mergeticklists(self.manualticks,
2335 self.parter.defaultpart(min/self.divisor,
2336 max/self.divisor,
2337 not self.fixmin,
2338 not self.fixmax))
2339 worse = 0
2340 nextpart = self.parter.lesspart
2341 while nextpart is not None:
2342 newticks = nextpart()
2343 if newticks is not None:
2344 newticks = _mergeticklists(self.manualticks, newticks)
2345 if first:
2346 bestrate = self.rater.rateticks(self, self.ticks, self.density)
2347 bestrate += self.rater.raterange(self.convert(float(self.ticks[-1])/self.divisor)-
2348 self.convert(float(self.ticks[0])/self.divisor), 1)
2349 variants = [[bestrate, self.ticks]]
2350 first = 0
2351 newrate = self.rater.rateticks(self, newticks, self.density)
2352 newrate += self.rater.raterange(self.convert(float(newticks[-1])/self.divisor)-
2353 self.convert(float(newticks[0])/self.divisor), 1)
2354 variants.append([newrate, newticks])
2355 if newrate < bestrate:
2356 bestrate = newrate
2357 worse = 0
2358 else:
2359 worse += 1
2360 else:
2361 worse += 1
2362 if worse == self.maxworse and nextpart == self.parter.lesspart:
2363 worse = 0
2364 nextpart = self.parter.morepart
2365 if worse == self.maxworse and nextpart == self.parter.morepart:
2366 nextpart = None
2367 else:
2368 self.ticks =self.manualticks
2370 # rating, when several choises are available
2371 if not first:
2372 variants.sort()
2373 if self.painter is not None:
2374 i = 0
2375 bestrate = None
2376 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
2377 saverange = self._getrange()
2378 self.ticks = variants[i][1]
2379 if len(self.ticks):
2380 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2381 self.texter.labels(self.ticks)
2382 ac = self.painter.paint(axispos, self)
2383 ratelayout = self.rater.ratelayout(ac, self.density)
2384 if ratelayout is not None:
2385 variants[i][0] += ratelayout
2386 variants[i].append(ac)
2387 else:
2388 variants[i][0] = None
2389 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
2390 bestrate = variants[i][0]
2391 self._forcerange(saverange)
2392 i += 1
2393 if bestrate is None:
2394 raise RuntimeError("no valid axis partitioning found")
2395 variants = [variant for variant in variants[:i] if variant[0] is not None]
2396 variants.sort()
2397 self.ticks = variants[0][1]
2398 if len(self.ticks):
2399 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2400 self.axiscanvas = variants[0][2]
2401 else:
2402 self.ticks = variants[0][1]
2403 self.texter.labels(self.ticks)
2404 if len(self.ticks):
2405 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2406 self.axiscanvas = axiscanvas()
2407 else:
2408 if len(self.ticks):
2409 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2410 self.texter.labels(self.ticks)
2411 if self.painter is not None:
2412 self.axiscanvas = self.painter.paint(axispos, self)
2413 else:
2414 self.axiscanvas = axiscanvas()
2416 def createlinkaxis(self, **args):
2417 return linkaxis(self, **args)
2420 class linaxis(_axis, _linmap):
2421 """implementation of a linear axis"""
2423 __implements__ = _Iaxis
2425 def __init__(self, parter=autolinparter(), rater=axisrater(), **args):
2426 """initializes the instance
2427 - the parter attribute implements _Iparter
2428 - manualticks and the partitioner results are mixed
2429 by _mergeticklists
2430 - the rater implements _Irater and is used to rate different
2431 tick lists created by the partitioner (after merging with
2432 manully set ticks)
2433 - futher keyword arguments are passed to _axis"""
2434 _axis.__init__(self, **args)
2435 if self.fixmin and self.fixmax:
2436 self.relsize = self.max - self.min
2437 self.parter = parter
2438 self.rater = rater
2441 class logaxis(_axis, _logmap):
2442 """implementation of a logarithmic axis"""
2444 __implements__ = _Iaxis
2446 def __init__(self, parter=autologparter(), rater=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
2447 """initializes the instance
2448 - the parter attribute implements _Iparter
2449 - manualticks and the partitioner results are mixed
2450 by _mergeticklists
2451 - the rater implements _Irater and is used to rate different
2452 tick lists created by the partitioner (after merging with
2453 manully set ticks)
2454 - futher keyword arguments are passed to _axis"""
2455 _axis.__init__(self, **args)
2456 if self.fixmin and self.fixmax:
2457 self.relsize = math.log(self.max) - math.log(self.min)
2458 self.parter = parter
2459 self.rater = rater
2462 class linkaxis:
2463 """a axis linked to an already existing regular axis
2464 - almost all properties of the axis are "copied" from the
2465 axis this axis is linked to
2466 - usually, linked axis are used to create an axis to an
2467 existing axis with different painting properties; linked
2468 axis can be used to plot an axis twice at the opposite
2469 sides of a graphxy or even to share an axis between
2470 different graphs!"""
2472 __implements__ = _Iaxis
2474 def __init__(self, linkedaxis, painter=linkaxispainter()):
2475 """initializes the instance
2476 - it gets a axis this linkaxis is linked to
2477 - it gets a painter to be used for this linked axis"""
2478 self.linkedaxis = linkedaxis
2479 self.painter = painter
2480 self.axiscanvas = None
2482 def __getattr__(self, attr):
2483 """access to unkown attributes are handed over to the
2484 axis this linkaxis is linked to"""
2485 return getattr(self.linkedaxis, attr)
2487 def finish(self, axispos):
2488 """finishes the axis
2489 - instead of performing the hole finish process
2490 (paritioning, rating, etc.) just a painter call
2491 is performed"""
2492 if self.axiscanvas is None:
2493 if self.linkedaxis.axiscanvas is None:
2494 raise RuntimeError("link axis finish method called before the finish method of the original axis")
2495 self.axiscanvas = self.painter.paint(axispos, self)
2498 class splitaxis:
2499 """implementation of a split axis
2500 - a split axis contains several (sub-)axes with
2501 non-overlapping data ranges -- between these subaxes
2502 the axis is "splitted"
2503 - (just to get sure: a splitaxis can contain other
2504 splitaxes as its subaxes)
2505 - a splitaxis implements the _Iaxispos for its subaxes
2506 by inheritance from _subaxispos"""
2508 __implements__ = _Iaxis, _Iaxispos
2510 def __init__(self, subaxes, splitlist=[0.5], splitdist=0.1, relsizesplitdist=1,
2511 title=None, painter=splitaxispainter()):
2512 """initializes the instance
2513 - subaxes is a list of subaxes
2514 - splitlist is a list of graph coordinates, where the splitting
2515 of the main axis should be performed; if the list isn't long enough
2516 for the subaxes, missing entries are considered to be None
2517 - splitdist is the size of the splitting in graph coordinates, when
2518 the associated splitlist entry is not None
2519 - relsizesplitdist: a None entry in splitlist means, that the
2520 position of the splitting should be calculated out of the
2521 relsize values of conrtibuting subaxes (the size of the
2522 splitting is relsizesplitdist in values of the relsize values
2523 of the axes)
2524 - title is the title of the axis as a string
2525 - painter is the painter of the axis; it should be specialized to
2526 the splitaxis
2527 - the relsize of the splitaxis is the sum of the relsizes of the
2528 subaxes including the relsizesplitdist"""
2529 self.subaxes = subaxes
2530 self.painter = painter
2531 self.title = title
2532 self.splitlist = splitlist
2533 for subaxis in self.subaxes:
2534 subaxis.vmin = None
2535 subaxis.vmax = None
2536 self.subaxes[0].vmin = 0
2537 self.subaxes[0].vminover = None
2538 self.subaxes[-1].vmax = 1
2539 self.subaxes[-1].vmaxover = None
2540 for i in xrange(len(self.splitlist)):
2541 if self.splitlist[i] is not None:
2542 self.subaxes[i].vmax = self.splitlist[i] - 0.5*splitdist
2543 self.subaxes[i].vmaxover = self.splitlist[i]
2544 self.subaxes[i+1].vmin = self.splitlist[i] + 0.5*splitdist
2545 self.subaxes[i+1].vminover = self.splitlist[i]
2546 i = 0
2547 while i < len(self.subaxes):
2548 if self.subaxes[i].vmax is None:
2549 j = relsize = relsize2 = 0
2550 while self.subaxes[i + j].vmax is None:
2551 relsize += self.subaxes[i + j].relsize + relsizesplitdist
2552 j += 1
2553 relsize += self.subaxes[i + j].relsize
2554 vleft = self.subaxes[i].vmin
2555 vright = self.subaxes[i + j].vmax
2556 for k in range(i, i + j):
2557 relsize2 += self.subaxes[k].relsize
2558 self.subaxes[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
2559 relsize2 += 0.5 * relsizesplitdist
2560 self.subaxes[k].vmaxover = self.subaxes[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
2561 relsize2 += 0.5 * relsizesplitdist
2562 self.subaxes[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
2563 if i == 0 and i + j + 1 == len(self.subaxes):
2564 self.relsize = relsize
2565 i += j + 1
2566 else:
2567 i += 1
2569 self.fixmin = self.subaxes[0].fixmin
2570 if self.fixmin:
2571 self.min = self.subaxes[0].min
2572 self.fixmax = self.subaxes[-1].fixmax
2573 if self.fixmax:
2574 self.max = self.subaxes[-1].max
2576 self.axiscanvas = None
2578 def getrange(self):
2579 min = self.subaxes[0].getrange()
2580 max = self.subaxes[-1].getrange()
2581 try:
2582 return min[0], max[1]
2583 except TypeError:
2584 return None
2586 def setrange(self, min, max):
2587 self.subaxes[0].setrange(min, None)
2588 self.subaxes[-1].setrange(None, max)
2590 def adjustrange(self, *args, **kwargs):
2591 self.subaxes[0].adjustrange(*args, **kwargs)
2592 self.subaxes[-1].adjustrange(*args, **kwargs)
2594 def convert(self, value):
2595 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2596 if value < self.subaxes[0].max:
2597 return self.subaxes[0].vmin + self.subaxes[0].convert(value)*(self.subaxes[0].vmax-self.subaxes[0].vmin)
2598 for axis in self.subaxes[1:-1]:
2599 if value > axis.min and value < axis.max:
2600 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
2601 if value > self.subaxes[-1].min:
2602 return self.subaxes[-1].vmin + self.subaxes[-1].convert(value)*(self.subaxes[-1].vmax-self.subaxes[-1].vmin)
2603 raise ValueError("value couldn't be assigned to a split region")
2605 def finish(self, axispos):
2606 if self.axiscanvas is None:
2607 self.axiscanvas = self.painter.paint(axispos, self)
2609 def createlinkaxis(self, **args):
2610 return linksplitaxis(self, **args)
2613 class omitsubaxispainter: pass
2615 class linksplitaxis(linkaxis):
2616 """a splitaxis linked to an already existing splitaxis
2617 - inherits the access to a linked axis -- as before,
2618 basically only the painter is replaced
2619 - it takes care of the creation of linked axes of
2620 the subaxes"""
2622 __implements__ = _Iaxis
2624 def __init__(self, linkedaxis, painter=linksplitaxispainter(), subaxispainter=omitsubaxispainter):
2625 """initializes the instance
2626 - linkedaxis is the axis this axis becomes linked to
2627 - painter is axispainter instance for this linked axis
2628 - subaxispainter is a changeable painter to be used for linked
2629 subaxes; if omitsubaxispainter the createlinkaxis method of
2630 the subaxis are called without a painter parameter"""
2631 linkaxis.__init__(self, linkedaxis, painter=painter)
2632 self.subaxes = []
2633 for subaxis in linkedaxis.subaxes:
2634 painter = attr.selectattr(subaxispainter, len(self.subaxes), len(linkedaxis.subaxes))
2635 if painter is omitsubaxispainter:
2636 self.subaxes.append(subaxis.createlinkaxis())
2637 else:
2638 self.subaxes.append(subaxis.createlinkaxis(painter=painter))
2641 class baraxis:
2642 """implementation of a axis for bar graphs
2643 - a bar axes is different from a splitaxis by the way it
2644 selects its subaxes: the convert method gets a list,
2645 where the first entry is a name selecting a subaxis out
2646 of a list; instead of the term "bar" or "subaxis" the term
2647 "item" will be used here
2648 - the baraxis stores a list of names be identify the items;
2649 the names might be of any time (strings, integers, etc.);
2650 the names can be printed as the titles for the items, but
2651 alternatively the names might be transformed by the texts
2652 dictionary, which maps a name to a text to be used to label
2653 the items in the painter
2654 - usually, there is only one subaxis, which is used as
2655 the subaxis for all items
2656 - alternatively it is also possible to use another baraxis
2657 as a multisubaxis; it is copied via the createsubaxis
2658 method whenever another subaxis is needed (by that a
2659 nested bar axis with a different number of subbars at
2660 each item can be created)
2661 - any axis can be a subaxis of a baraxis; if no subaxis
2662 is specified at all, the baraxis simulates a linear
2663 subaxis with a fixed range of 0 to 1
2664 - a splitaxis implements the _Iaxispos for its subaxes
2665 by inheritance from _subaxispos when the multisubaxis
2666 feature is turned on"""
2668 def __init__(self, subaxis=None, multisubaxis=None, title=None,
2669 dist=0.5, firstdist=None, lastdist=None, names=None,
2670 texts={}, painter=baraxispainter()):
2671 """initialize the instance
2672 - subaxis contains a axis to be used as the subaxis
2673 for all items
2674 - multisubaxis might contain another baraxis instance
2675 to be used to construct a new subaxis for each item;
2676 (by that a nested bar axis with a different number
2677 of subbars at each item can be created)
2678 - only one of subaxis or multisubaxis can be set; if neither
2679 of them is set, the baraxis behaves like having a linaxis
2680 as its subaxis with a fixed range 0 to 1
2681 - the title attribute contains the axis title as a string
2682 - the dist is a relsize to be used as the distance between
2683 the items
2684 - the firstdist and lastdist are the distance before the
2685 first and after the last item, respectively; when set
2686 to None (the default), 0.5*dist is used
2687 - names is a predefined list of names to identify the
2688 items; if set, the name list is fixed
2689 - texts is a dictionary transforming a name to a text in
2690 the painter; if a name isn't found in the dictionary
2691 it gets used itself
2692 - the relsize of the baraxis is the sum of the
2693 relsizes including all distances between the items"""
2694 self.dist = dist
2695 if firstdist is not None:
2696 self.firstdist = firstdist
2697 else:
2698 self.firstdist = 0.5 * dist
2699 if lastdist is not None:
2700 self.lastdist = lastdist
2701 else:
2702 self.lastdist = 0.5 * dist
2703 self.relsizes = None
2704 self.fixnames = 0
2705 self.names = []
2706 for name in helper.ensuresequence(names):
2707 self.setname(name)
2708 self.fixnames = names is not None
2709 self.multisubaxis = multisubaxis
2710 if self.multisubaxis is not None:
2711 if subaxis is not None:
2712 raise RuntimeError("either use subaxis or multisubaxis")
2713 self.subaxis = [self.createsubaxis() for name in self.names]
2714 else:
2715 self.subaxis = subaxis
2716 self.title = title
2717 self.fixnames = 0
2718 self.texts = texts
2719 self.painter = painter
2720 self.axiscanvas = None
2722 def createsubaxis(self):
2723 return baraxis(subaxis=self.multisubaxis.subaxis,
2724 multisubaxis=self.multisubaxis.multisubaxis,
2725 title=self.multisubaxis.title,
2726 dist=self.multisubaxis.dist,
2727 firstdist=self.multisubaxis.firstdist,
2728 lastdist=self.multisubaxis.lastdist,
2729 names=self.multisubaxis.names,
2730 texts=self.multisubaxis.texts,
2731 painter=self.multisubaxis.painter)
2733 def getrange(self):
2734 # TODO: we do not yet have a proper range handling for a baraxis
2735 return None
2737 def setrange(self, min=None, max=None):
2738 # TODO: we do not yet have a proper range handling for a baraxis
2739 raise RuntimeError("range handling for a baraxis is not implemented")
2741 def setname(self, name, *subnames):
2742 """add a name to identify an item at the baraxis
2743 - by using subnames, nested name definitions are
2744 possible
2745 - a style (or the user itself) might use this to
2746 insert new items into a baraxis
2747 - setting self.relsizes to None forces later recalculation"""
2748 if not self.fixnames:
2749 if name not in self.names:
2750 self.relsizes = None
2751 self.names.append(name)
2752 if self.multisubaxis is not None:
2753 self.subaxis.append(self.createsubaxis())
2754 if (not self.fixnames or name in self.names) and len(subnames):
2755 if self.multisubaxis is not None:
2756 if self.subaxis[self.names.index(name)].setname(*subnames):
2757 self.relsizes = None
2758 else:
2759 if self.subaxis.setname(*subnames):
2760 self.relsizes = None
2761 return self.relsizes is not None
2763 def adjustrange(self, points, index, subnames=None):
2764 if subnames is None:
2765 subnames = []
2766 for point in points:
2767 self.setname(point[index], *subnames)
2769 def updaterelsizes(self):
2770 # guess what it does: it recalculates relsize attribute
2771 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
2772 self.relsizes[-1] += self.lastdist - self.dist
2773 if self.multisubaxis is not None:
2774 subrelsize = 0
2775 for i in range(1, len(self.relsizes)):
2776 self.subaxis[i-1].updaterelsizes()
2777 subrelsize += self.subaxis[i-1].relsizes[-1]
2778 self.relsizes[i] += subrelsize
2779 else:
2780 if self.subaxis is None:
2781 subrelsize = 1
2782 else:
2783 self.subaxis.updaterelsizes()
2784 subrelsize = self.subaxis.relsizes[-1]
2785 for i in range(1, len(self.relsizes)):
2786 self.relsizes[i] += i * subrelsize
2788 def convert(self, value):
2789 """baraxis convert method
2790 - the value should be a list, where the first entry is
2791 a member of the names (set in the constructor or by the
2792 setname method); this first entry identifies an item in
2793 the baraxis
2794 - following values are passed to the appropriate subaxis
2795 convert method
2796 - when there is no subaxis, the convert method will behave
2797 like having a linaxis from 0 to 1 as subaxis"""
2798 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2799 if not self.relsizes:
2800 self.updaterelsizes()
2801 pos = self.names.index(value[0])
2802 if len(value) == 2:
2803 if self.subaxis is None:
2804 subvalue = value[1]
2805 else:
2806 if self.multisubaxis is not None:
2807 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
2808 else:
2809 subvalue = value[1] * self.subaxis.relsizes[-1]
2810 else:
2811 if self.multisubaxis is not None:
2812 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
2813 else:
2814 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
2815 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
2817 def finish(self, axispos):
2818 if self.axiscanvas is None:
2819 if self.multisubaxis is not None:
2820 for name, subaxis in zip(self.names, self.subaxis):
2821 subaxis.vmin = self.convert((name, 0))
2822 subaxis.vmax = self.convert((name, 1))
2823 self.axiscanvas = self.painter.paint(axispos, self)
2825 def createlinkaxis(self, **args):
2826 return linkbaraxis(self, **args)
2829 class linkbaraxis(linkaxis):
2830 """a baraxis linked to an already existing baraxis
2831 - inherits the access to a linked axis -- as before,
2832 basically only the painter is replaced
2833 - it must take care of the creation of linked axes of
2834 the subaxes"""
2836 __implements__ = _Iaxis
2838 def __init__(self, linkedaxis, painter=linkbaraxispainter()):
2839 """initializes the instance
2840 - it gets a axis this linkaxis is linked to
2841 - it gets a painter to be used for this linked axis"""
2842 linkaxis.__init__(self, linkedaxis, painter=painter)
2843 if self.multisubaxis is not None:
2844 self.subaxis = [subaxis.createlinkaxis() for subaxis in self.linkedaxis.subaxis]
2845 elif self.subaxis is not None:
2846 self.subaxis = self.subaxis.createlinkaxis()
2849 def pathaxis(path, axis, **kwargs):
2850 """creates an axiscanvas for an axis along a path"""
2851 mypathaxispos = pathaxispos(path, axis.convert, **kwargs)
2852 axis.finish(mypathaxispos)
2853 return axis.axiscanvas
2855 ################################################################################
2856 # graph key
2857 ################################################################################
2859 class key:
2861 defaulttextattrs = [textmodule.vshift.mathaxis]
2863 def __init__(self, dist="0.2 cm", pos="tr", hinside=1, vinside=1, hdist="0.6 cm", vdist="0.4 cm",
2864 symbolwidth="0.5 cm", symbolheight="0.25 cm", symbolspace="0.2 cm",
2865 textattrs=[]):
2866 self.dist_str = dist
2867 self.pos = pos
2868 self.hinside = hinside
2869 self.vinside = vinside
2870 self.hdist_str = hdist
2871 self.vdist_str = vdist
2872 self.symbolwidth_str = symbolwidth
2873 self.symbolheight_str = symbolheight
2874 self.symbolspace_str = symbolspace
2875 self.textattrs = textattrs
2876 if self.pos in ("tr", "rt"):
2877 self.right = 1
2878 self.top = 1
2879 elif self.pos in ("br", "rb"):
2880 self.right = 1
2881 self.top = 0
2882 elif self.pos in ("tl", "lt"):
2883 self.right = 0
2884 self.top = 1
2885 elif self.pos in ("bl", "lb"):
2886 self.right = 0
2887 self.top = 0
2888 else:
2889 raise RuntimeError("invalid pos attribute")
2891 def paint(self, plotdata):
2892 "creates the layout of the key"
2893 c = canvas.canvas()
2894 self.dist_pt = unit.topt(unit.length(self.dist_str, default_type="v"))
2895 self.hdist_pt = unit.topt(unit.length(self.hdist_str, default_type="v"))
2896 self.vdist_pt = unit.topt(unit.length(self.vdist_str, default_type="v"))
2897 self.symbolwidth_pt = unit.topt(unit.length(self.symbolwidth_str, default_type="v"))
2898 self.symbolheight_pt = unit.topt(unit.length(self.symbolheight_str, default_type="v"))
2899 self.symbolspace_pt = unit.topt(unit.length(self.symbolspace_str, default_type="v"))
2900 titles = []
2901 for plotdat in plotdata:
2902 titles.append(c.texrunner.text_pt(0, 0, plotdat.title, self.defaulttextattrs + self.textattrs))
2903 box.tile_pt(titles, self.dist_pt, 0, -1)
2904 box.linealignequal_pt(titles, self.symbolwidth_pt + self.symbolspace_pt, 1, 0)
2905 for plotdat, title in zip(plotdata, titles):
2906 plotdat.style.key_pt(c, 0, -0.5 * self.symbolheight_pt + title.center[1],
2907 self.symbolwidth_pt, self.symbolheight_pt, plotdat)
2908 c.insert(title)
2909 return c
2912 ################################################################################
2913 # graph
2914 ################################################################################
2917 class lineaxispos:
2918 """an axispos linear along a line with a fix direction for the ticks"""
2920 __implements__ = _Iaxispos
2922 def __init__(self, convert, x1, y1, x2, y2, fixtickdirection):
2923 """initializes the instance
2924 - only the convert method is needed from the axis
2925 - x1, y1, x2, y2 are PyX lengths (start and end position of the line)
2926 - fixtickdirection is a tuple tick direction (fixed along the line)"""
2927 self.convert = convert
2928 self.x1 = x1
2929 self.y1 = y1
2930 self.x2 = x2
2931 self.y2 = y2
2932 self.x1_pt = unit.topt(x1)
2933 self.y1_pt = unit.topt(y1)
2934 self.x2_pt = unit.topt(x2)
2935 self.y2_pt = unit.topt(y2)
2936 self.fixtickdirection = fixtickdirection
2938 def vbasepath(self, v1=None, v2=None):
2939 if v1 is None:
2940 v1 = 0
2941 if v2 is None:
2942 v2 = 1
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 basepath(self, x1=None, x2=None):
2949 if x1 is None:
2950 v1 = 0
2951 else:
2952 v1 = self.convert(x1)
2953 if x2 is None:
2954 v2 = 1
2955 else:
2956 v2 = self.convert(x2)
2957 return path.line_pt((1-v1)*self.x1_pt+v1*self.x2_pt,
2958 (1-v1)*self.y1_pt+v1*self.y2_pt,
2959 (1-v2)*self.x1_pt+v2*self.x2_pt,
2960 (1-v2)*self.y1_pt+v2*self.y2_pt)
2962 def gridpath(self, x):
2963 raise RuntimeError("gridpath not available")
2965 def vgridpath(self, v):
2966 raise RuntimeError("gridpath not available")
2968 def vtickpoint_pt(self, v):
2969 return (1-v)*self.x1_pt+v*self.x2_pt, (1-v)*self.y1_pt+v*self.y2_pt
2971 def vtickpoint(self, v):
2972 return (1-v)*self.x1+v*self.x2, (1-v)*self.y1+v*self.y2
2974 def tickpoint_pt(self, x):
2975 v = self.convert(x)
2976 return (1-v)*self.x1_pt+v*self.x2_pt, (1-v)*self.y1_pt+v*self.y2_pt
2978 def tickpoint(self, x):
2979 v = self.convert(x)
2980 return (1-v)*self.x1+v*self.x2, (1-v)*self.y1+v*self.y2
2982 def tickdirection(self, x):
2983 return self.fixtickdirection
2985 def vtickdirection(self, v):
2986 return self.fixtickdirection
2989 class lineaxisposlinegrid(lineaxispos):
2990 """an axispos linear along a line with a fix direction for the ticks
2991 with support for grid lines for a rectangular graphs"""
2993 __implements__ = _Iaxispos
2995 def __init__(self, convert, x1, y1, x2, y2, fixtickdirection, startgridlength, endgridlength):
2996 """initializes the instance
2997 - only the convert method is needed from the axis
2998 - x1, y1, x2, y2 are PyX lengths (start and end position of the line)
2999 - fixtickdirection is a tuple tick direction (fixed along the line)
3000 - startgridlength and endgridlength are PyX lengths for the starting
3001 and end point of the grid, respectively; the gridpath is a line along
3002 the fixtickdirection"""
3003 lineaxispos.__init__(self, convert, x1, y1, x2, y2, fixtickdirection)
3004 self.startgridlength = startgridlength
3005 self.endgridlength = endgridlength
3006 self.startgridlength_pt = unit.topt(self.startgridlength)
3007 self.endgridlength_pt = unit.topt(self.endgridlength)
3009 def gridpath(self, x):
3010 v = self.convert(x)
3011 return path.line_pt((1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.startgridlength_pt,
3012 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.startgridlength_pt,
3013 (1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.endgridlength_pt,
3014 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.endgridlength_pt)
3016 def vgridpath(self, v):
3017 return path.line_pt((1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.startgridlength_pt,
3018 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.startgridlength_pt,
3019 (1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.endgridlength_pt,
3020 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.endgridlength_pt)
3023 class graphxy(canvas.canvas):
3025 axisnames = "x", "y"
3027 class axisposdata:
3029 def __init__(self, type, axispos, tickdirection):
3031 - type == 0: x-axis; type == 1: y-axis
3032 - axispos_pt is the y or x position of the x-axis or y-axis
3033 in postscript points, respectively
3034 - axispos is analogous to axispos, but as a PyX length
3035 - dx and dy is the tick direction
3037 self.type = type
3038 self.axispos = axispos
3039 self.axispos_pt = unit.topt(axispos)
3040 self.tickdirection = tickdirection
3042 def plot(self, data, style=None):
3043 if self.haslayout:
3044 raise RuntimeError("layout setup was already performed")
3045 try:
3046 for d in data:
3047 pass
3048 except:
3049 usedata = [data]
3050 else:
3051 usedata = data
3052 if style is None:
3053 for d in usedata:
3054 if style is None:
3055 style = d.defaultstyle
3056 elif style != d.defaultstyle:
3057 raise RuntimeError("defaultstyles differ")
3058 for d in usedata:
3059 d.setstyle(self, style)
3060 self.plotdata.append(d)
3061 return data
3063 def pos_pt(self, x, y, xaxis=None, yaxis=None):
3064 if xaxis is None:
3065 xaxis = self.axes["x"]
3066 if yaxis is None:
3067 yaxis = self.axes["y"]
3068 return self.xpos_pt + xaxis.convert(x)*self.width_pt, self.ypos_pt + yaxis.convert(y)*self.height_pt
3070 def pos(self, x, y, xaxis=None, yaxis=None):
3071 if xaxis is None:
3072 xaxis = self.axes["x"]
3073 if yaxis is None:
3074 yaxis = self.axes["y"]
3075 return self.xpos + xaxis.convert(x)*self.width, self.ypos + yaxis.convert(y)*self.height
3077 def vpos_pt(self, vx, vy):
3078 return self.xpos_pt + vx*self.width_pt, self.ypos_pt + vy*self.height_pt
3080 def vpos(self, vx, vy):
3081 return self.xpos + vx*self.width, self.ypos + vy*self.height
3083 def vgeodesic(self, vx1, vy1, vx2, vy2):
3084 """returns a geodesic path between two points in graph coordinates"""
3085 return path.line_pt(self.xpos_pt + vx1*self.width_pt,
3086 self.ypos_pt + vy1*self.height_pt,
3087 self.xpos_pt + vx2*self.width_pt,
3088 self.ypos_pt + vy2*self.height_pt)
3090 def vgeodesic_el(self, vx1, vy1, vx2, vy2):
3091 """returns a geodesic path element between two points in graph coordinates"""
3092 return path.lineto_pt(self.xpos_pt + vx2*self.width_pt,
3093 self.ypos_pt + vy2*self.height_pt)
3095 def vcap_pt(self, direction, length_pt, vx, vy):
3096 """returns an error cap path for a given direction, lengths and
3097 point in graph coordinates"""
3098 if direction == "x":
3099 return path.line_pt(self.xpos_pt + vx*self.width_pt - 0.5*length_pt,
3100 self.ypos_pt + vy*self.height_pt,
3101 self.xpos_pt + vx*self.width_pt + 0.5*length_pt,
3102 self.ypos_pt + vy*self.height_pt)
3103 elif direction == "y":
3104 return path.line_pt(self.xpos_pt + vx*self.width_pt,
3105 self.ypos_pt + vy*self.height_pt - 0.5*length_pt,
3106 self.xpos_pt + vx*self.width_pt,
3107 self.ypos_pt + vy*self.height_pt + 0.5*length_pt)
3108 else:
3109 raise ValueError("direction invalid")
3111 def keynum(self, key):
3112 try:
3113 while key[0] in string.letters:
3114 key = key[1:]
3115 return int(key)
3116 except IndexError:
3117 return 1
3119 def removedomethod(self, method):
3120 hadmethod = 0
3121 while 1:
3122 try:
3123 self.domethods.remove(method)
3124 hadmethod = 1
3125 except ValueError:
3126 return hadmethod
3128 def dolayout(self):
3129 if not self.removedomethod(self.dolayout): return
3131 # count the usage of styles and perform selects
3132 styletotal = {}
3133 for data in self.plotdata:
3134 try:
3135 styletotal[id(data.style)] += 1
3136 except:
3137 styletotal[id(data.style)] = 1
3138 styleindex = {}
3139 for data in self.plotdata:
3140 try:
3141 styleindex[id(data.style)] += 1
3142 except:
3143 styleindex[id(data.style)] = 0
3144 data.selectstyle(self, styleindex[id(data.style)], styletotal[id(data.style)])
3146 # adjust the axes ranges
3147 for step in range(3):
3148 for data in self.plotdata:
3149 data.adjustaxes(self, step)
3151 # finish all axes
3152 axesdist = unit.length(self.axesdist_str, default_type="v")
3153 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.axisnames[0])
3154 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.axisnames[1])
3155 xaxisextents = [0, 0]
3156 yaxisextents = [0, 0]
3157 needxaxisdist = [0, 0]
3158 needyaxisdist = [0, 0]
3159 items = list(self.axes.items())
3160 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3161 for key, axis in items:
3162 num = self.keynum(key)
3163 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3164 num3 = 2 * (num % 2) - 1 # x1 -> 1, x2 -> -1, x3 -> 1, x4 -> -1, ...
3165 if XPattern.match(key):
3166 if needxaxisdist[num2]:
3167 xaxisextents[num2] += axesdist
3168 self.axespos[key] = lineaxisposlinegrid(self.axes[key].convert,
3169 self.xpos,
3170 self.ypos + num2*self.height - num3*xaxisextents[num2],
3171 self.xpos + self.width,
3172 self.ypos + num2*self.height - num3*xaxisextents[num2],
3173 (0, num3),
3174 xaxisextents[num2], xaxisextents[num2] + self.height)
3175 if num == 1:
3176 self.xbasepath = self.axespos[key].basepath
3177 self.xvbasepath = self.axespos[key].vbasepath
3178 self.xgridpath = self.axespos[key].gridpath
3179 self.xvgridpath = self.axespos[key].vgridpath
3180 self.xtickpoint_pt = self.axespos[key].tickpoint_pt
3181 self.xtickpoint = self.axespos[key].tickpoint
3182 self.xvtickpoint_pt = self.axespos[key].vtickpoint_pt
3183 self.xvtickpoint = self.axespos[key].tickpoint
3184 self.xtickdirection = self.axespos[key].tickdirection
3185 self.xvtickdirection = self.axespos[key].vtickdirection
3186 elif YPattern.match(key):
3187 if needyaxisdist[num2]:
3188 yaxisextents[num2] += axesdist
3189 self.axespos[key] = lineaxisposlinegrid(self.axes[key].convert,
3190 self.xpos + num2*self.width - num3*yaxisextents[num2],
3191 self.ypos,
3192 self.xpos + num2*self.width - num3*yaxisextents[num2],
3193 self.ypos + self.height,
3194 (num3, 0),
3195 yaxisextents[num2], yaxisextents[num2] + self.width)
3196 if num == 1:
3197 self.ybasepath = self.axespos[key].basepath
3198 self.yvbasepath = self.axespos[key].vbasepath
3199 self.ygridpath = self.axespos[key].gridpath
3200 self.yvgridpath = self.axespos[key].vgridpath
3201 self.ytickpoint_pt = self.axespos[key].tickpoint_pt
3202 self.ytickpoint = self.axespos[key].tickpoint
3203 self.yvtickpoint_pt = self.axespos[key].vtickpoint_pt
3204 self.yvtickpoint = self.axespos[key].tickpoint
3205 self.ytickdirection = self.axespos[key].tickdirection
3206 self.yvtickdirection = self.axespos[key].vtickdirection
3207 else:
3208 raise ValueError("Axis key '%s' not allowed" % key)
3209 axis.finish(self.axespos[key])
3210 if XPattern.match(key):
3211 xaxisextents[num2] += axis.axiscanvas.extent
3212 needxaxisdist[num2] = 1
3213 if YPattern.match(key):
3214 yaxisextents[num2] += axis.axiscanvas.extent
3215 needyaxisdist[num2] = 1
3216 self.haslayout = 1
3218 def dobackground(self):
3219 self.dolayout()
3220 if not self.removedomethod(self.dobackground): return
3221 if self.backgroundattrs is not None:
3222 self.draw(path.rect_pt(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt),
3223 helper.ensurelist(self.backgroundattrs))
3225 def doaxes(self):
3226 self.dolayout()
3227 if not self.removedomethod(self.doaxes): return
3228 for axis in self.axes.values():
3229 self.insert(axis.axiscanvas)
3231 def dodata(self):
3232 self.dolayout()
3233 if not self.removedomethod(self.dodata): return
3234 for data in self.plotdata:
3235 data.draw(self)
3237 def dokey(self):
3238 self.dolayout()
3239 if not self.removedomethod(self.dokey): return
3240 if self.key is not None:
3241 c = self.key.paint(self.plotdata)
3242 bbox = c.bbox()
3243 if self.key.right:
3244 if self.key.hinside:
3245 x = self.xpos_pt + self.width_pt - bbox.urx - self.key.hdist_pt
3246 else:
3247 x = self.xpos_pt + self.width_pt - bbox.llx + self.key.hdist_pt
3248 else:
3249 if self.key.hinside:
3250 x = self.xpos_pt - bbox.llx + self.key.hdist_pt
3251 else:
3252 x = self.xpos_pt - bbox.urx - self.key.hdist_pt
3253 if self.key.top:
3254 if self.key.vinside:
3255 y = self.ypos_pt + self.height_pt - bbox.ury - self.key.vdist_pt
3256 else:
3257 y = self.ypos_pt + self.height_pt - bbox.lly + self.key.vdist_pt
3258 else:
3259 if self.key.vinside:
3260 y = self.ypos_pt - bbox.lly + self.key.vdist_pt
3261 else:
3262 y = self.ypos_pt - bbox.ury - self.key.vdist_pt
3263 self.insert(c, [trafomodule.translate_pt(x, y)])
3265 def finish(self):
3266 while len(self.domethods):
3267 self.domethods[0]()
3269 def initwidthheight(self, width, height, ratio):
3270 if (width is not None) and (height is None):
3271 self.width = unit.length(width)
3272 self.height = (1.0/ratio) * self.width
3273 elif (height is not None) and (width is None):
3274 self.height = unit.length(height)
3275 self.width = ratio * self.height
3276 else:
3277 self.width = unit.length(width)
3278 self.height = unit.length(height)
3279 self.width_pt = unit.topt(self.width)
3280 self.height_pt = unit.topt(self.height)
3281 if self.width_pt <= 0: raise ValueError("width <= 0")
3282 if self.height_pt <= 0: raise ValueError("height <= 0")
3284 def initaxes(self, axes, addlinkaxes=0):
3285 for key in self.axisnames:
3286 if not axes.has_key(key):
3287 axes[key] = linaxis()
3288 elif axes[key] is None:
3289 del axes[key]
3290 if addlinkaxes:
3291 if not axes.has_key(key + "2") and axes.has_key(key):
3292 axes[key + "2"] = axes[key].createlinkaxis()
3293 elif axes[key + "2"] is None:
3294 del axes[key + "2"]
3295 self.axes = axes
3297 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
3298 key=None, backgroundattrs=None, axesdist="0.8 cm", **axes):
3299 canvas.canvas.__init__(self)
3300 self.xpos = unit.length(xpos)
3301 self.ypos = unit.length(ypos)
3302 self.xpos_pt = unit.topt(self.xpos)
3303 self.ypos_pt = unit.topt(self.ypos)
3304 self.initwidthheight(width, height, ratio)
3305 self.initaxes(axes, 1)
3306 self.axescanvas = {}
3307 self.axespos = {}
3308 self.key = key
3309 self.backgroundattrs = backgroundattrs
3310 self.axesdist_str = axesdist
3311 self.plotdata = []
3312 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata, self.dokey]
3313 self.haslayout = 0
3314 self.addkeys = []
3316 def bbox(self):
3317 self.finish()
3318 return canvas.canvas.bbox(self)
3320 def write(self, file):
3321 self.finish()
3322 canvas.canvas.write(self, file)
3326 # some thoughts, but deferred right now
3328 # class graphxyz(graphxy):
3330 # axisnames = "x", "y", "z"
3332 # def _vxtickpoint(self, axis, v):
3333 # return self._vpos(v, axis.vypos, axis.vzpos)
3335 # def _vytickpoint(self, axis, v):
3336 # return self._vpos(axis.vxpos, v, axis.vzpos)
3338 # def _vztickpoint(self, axis, v):
3339 # return self._vpos(axis.vxpos, axis.vypos, v)
3341 # def vxtickdirection(self, axis, v):
3342 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
3343 # x2, y2 = self._vpos(v, 0.5, 0)
3344 # dx, dy = x1 - x2, y1 - y2
3345 # norm = math.sqrt(dx*dx + dy*dy)
3346 # return dx/norm, dy/norm
3348 # def vytickdirection(self, axis, v):
3349 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
3350 # x2, y2 = self._vpos(0.5, v, 0)
3351 # dx, dy = x1 - x2, y1 - y2
3352 # norm = math.sqrt(dx*dx + dy*dy)
3353 # return dx/norm, dy/norm
3355 # def vztickdirection(self, axis, v):
3356 # return -1, 0
3357 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
3358 # x2, y2 = self._vpos(0.5, 0.5, v)
3359 # dx, dy = x1 - x2, y1 - y2
3360 # norm = math.sqrt(dx*dx + dy*dy)
3361 # return dx/norm, dy/norm
3363 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3364 # if xaxis is None: xaxis = self.axes["x"]
3365 # if yaxis is None: yaxis = self.axes["y"]
3366 # if zaxis is None: zaxis = self.axes["z"]
3367 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3369 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3370 # if xaxis is None: xaxis = self.axes["x"]
3371 # if yaxis is None: yaxis = self.axes["y"]
3372 # if zaxis is None: zaxis = self.axes["z"]
3373 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3375 # def _vpos(self, vx, vy, vz):
3376 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
3377 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
3378 # + self.a[2]*self.b[0]*(y-self.eye[1])
3379 # + self.a[1]*self.b[2]*(x-self.eye[0])
3380 # - self.a[2]*self.b[1]*(x-self.eye[0])
3381 # - self.a[0]*self.b[2]*(y-self.eye[1])
3382 # - self.a[1]*self.b[0]*(z-self.eye[2]))
3383 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
3384 # + self.eye[2]*self.b[0]*(y-self.eye[1])
3385 # + self.eye[1]*self.b[2]*(x-self.eye[0])
3386 # - self.eye[2]*self.b[1]*(x-self.eye[0])
3387 # - self.eye[0]*self.b[2]*(y-self.eye[1])
3388 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
3389 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
3390 # + self.a[2]*self.eye[0]*(y-self.eye[1])
3391 # + self.a[1]*self.eye[2]*(x-self.eye[0])
3392 # - self.a[2]*self.eye[1]*(x-self.eye[0])
3393 # - self.a[0]*self.eye[2]*(y-self.eye[1])
3394 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
3395 # return da/d0 + self._xpos, db/d0 + self._ypos
3397 # def vpos(self, vx, vy, vz):
3398 # tx, ty = self._vpos(vx, vy, vz)
3399 # return unit.t_pt(tx), unit.t_pt(ty)
3401 # def xbaseline(self, axis, x1, x2, xaxis=None):
3402 # if xaxis is None: xaxis = self.axes["x"]
3403 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2))
3405 # def ybaseline(self, axis, y1, y2, yaxis=None):
3406 # if yaxis is None: yaxis = self.axes["y"]
3407 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2))
3409 # def zbaseline(self, axis, z1, z2, zaxis=None):
3410 # if zaxis is None: zaxis = self.axes["z"]
3411 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2))
3413 # def vxbaseline(self, axis, v1, v2):
3414 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
3415 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
3416 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
3417 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
3419 # def vybaseline(self, axis, v1, v2):
3420 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
3421 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
3422 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
3423 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
3425 # def vzbaseline(self, axis, v1, v2):
3426 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
3427 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
3428 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
3429 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
3431 # def xgridpath(self, x, xaxis=None):
3432 # assert 0
3433 # if xaxis is None: xaxis = self.axes["x"]
3434 # v = xaxis.convert(x)
3435 # return path._line(self._xpos+v*self._width, self._ypos,
3436 # self._xpos+v*self._width, self._ypos+self._height)
3438 # def ygridpath(self, y, yaxis=None):
3439 # assert 0
3440 # if yaxis is None: yaxis = self.axes["y"]
3441 # v = yaxis.convert(y)
3442 # return path._line(self._xpos, self._ypos+v*self._height,
3443 # self._xpos+self._width, self._ypos+v*self._height)
3445 # def zgridpath(self, z, zaxis=None):
3446 # assert 0
3447 # if zaxis is None: zaxis = self.axes["z"]
3448 # v = zaxis.convert(z)
3449 # return path._line(self._xpos, self._zpos+v*self._height,
3450 # self._xpos+self._width, self._zpos+v*self._height)
3452 # def vxgridpath(self, v):
3453 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
3454 # path._lineto(*self._vpos(v, 0, 1)),
3455 # path._lineto(*self._vpos(v, 1, 1)),
3456 # path._lineto(*self._vpos(v, 1, 0)),
3457 # path.closepath())
3459 # def vygridpath(self, v):
3460 # return path.path(path._moveto(*self._vpos(0, v, 0)),
3461 # path._lineto(*self._vpos(0, v, 1)),
3462 # path._lineto(*self._vpos(1, v, 1)),
3463 # path._lineto(*self._vpos(1, v, 0)),
3464 # path.closepath())
3466 # def vzgridpath(self, v):
3467 # return path.path(path._moveto(*self._vpos(0, 0, v)),
3468 # path._lineto(*self._vpos(0, 1, v)),
3469 # path._lineto(*self._vpos(1, 1, v)),
3470 # path._lineto(*self._vpos(1, 0, v)),
3471 # path.closepath())
3473 # def _addpos(self, x, y, dx, dy):
3474 # assert 0
3475 # return x+dx, y+dy
3477 # def _connect(self, x1, y1, x2, y2):
3478 # assert 0
3479 # return path._lineto(x2, y2)
3481 # def doaxes(self):
3482 # self.dolayout()
3483 # if not self.removedomethod(self.doaxes): return
3484 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
3485 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.axisnames[0])
3486 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.axisnames[1])
3487 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.axisnames[2])
3488 # items = list(self.axes.items())
3489 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3490 # for key, axis in items:
3491 # num = self.keynum(key)
3492 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3493 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3494 # if XPattern.match(key):
3495 # axis.vypos = 0
3496 # axis.vzpos = 0
3497 # axis._vtickpoint = self._vxtickpoint
3498 # axis.vgridpath = self.vxgridpath
3499 # axis.vbaseline = self.vxbaseline
3500 # axis.vtickdirection = self.vxtickdirection
3501 # elif YPattern.match(key):
3502 # axis.vxpos = 0
3503 # axis.vzpos = 0
3504 # axis._vtickpoint = self._vytickpoint
3505 # axis.vgridpath = self.vygridpath
3506 # axis.vbaseline = self.vybaseline
3507 # axis.vtickdirection = self.vytickdirection
3508 # elif ZPattern.match(key):
3509 # axis.vxpos = 0
3510 # axis.vypos = 0
3511 # axis._vtickpoint = self._vztickpoint
3512 # axis.vgridpath = self.vzgridpath
3513 # axis.vbaseline = self.vzbaseline
3514 # axis.vtickdirection = self.vztickdirection
3515 # else:
3516 # raise ValueError("Axis key '%s' not allowed" % key)
3517 # if axis.painter is not None:
3518 # axis.dopaint(self)
3519 # # if XPattern.match(key):
3520 # # self._xaxisextents[num2] += axis._extent
3521 # # needxaxisdist[num2] = 1
3522 # # if YPattern.match(key):
3523 # # self._yaxisextents[num2] += axis._extent
3524 # # needyaxisdist[num2] = 1
3526 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
3527 # phi=30, theta=30, distance=1,
3528 # backgroundattrs=None, axesdist="0.8 cm", **axes):
3529 # canvas.canvas.__init__(self)
3530 # self.tex = tex
3531 # self.xpos = xpos
3532 # self.ypos = ypos
3533 # self._xpos = unit.topt(xpos)
3534 # self._ypos = unit.topt(ypos)
3535 # self._width = unit.topt(width)
3536 # self._height = unit.topt(height)
3537 # self._depth = unit.topt(depth)
3538 # self.width = width
3539 # self.height = height
3540 # self.depth = depth
3541 # if self._width <= 0: raise ValueError("width < 0")
3542 # if self._height <= 0: raise ValueError("height < 0")
3543 # if self._depth <= 0: raise ValueError("height < 0")
3544 # self._distance = distance*math.sqrt(self._width*self._width+
3545 # self._height*self._height+
3546 # self._depth*self._depth)
3547 # phi *= -math.pi/180
3548 # theta *= math.pi/180
3549 # self.a = (-math.sin(phi), math.cos(phi), 0)
3550 # self.b = (-math.cos(phi)*math.sin(theta),
3551 # -math.sin(phi)*math.sin(theta),
3552 # math.cos(theta))
3553 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
3554 # self._distance*math.sin(phi)*math.cos(theta),
3555 # self._distance*math.sin(theta))
3556 # self.initaxes(axes)
3557 # self.axesdist_str = axesdist
3558 # self.backgroundattrs = backgroundattrs
3560 # self.data = []
3561 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
3562 # self.haslayout = 0
3563 # self.defaultstyle = {}
3565 # def bbox(self):
3566 # self.finish()
3567 # return bbox._bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
3570 ################################################################################
3571 # styles
3572 ################################################################################
3575 class _style:
3577 def setdatapattern(self, graph, columns, pattern):
3578 for datakey in columns.keys():
3579 match = pattern.match(datakey)
3580 if match:
3581 # XXX match.groups()[0] must contain the full axisname
3582 axisname = match.groups()[0]
3583 index = columns[datakey]
3584 del columns[datakey]
3585 return graph.axes[axisname], index
3587 def key_pt(self, c, x_pt, y_pt, width_pt, height_pt, data):
3588 raise RuntimeError("style doesn't provide a key")
3591 class symbolline(_style):
3593 def cross(self, x_pt, y_pt, size_pt):
3594 return (path.moveto_pt(x_pt-0.5*size_pt, y_pt-0.5*size_pt),
3595 path.lineto_pt(x_pt+0.5*size_pt, y_pt+0.5*size_pt),
3596 path.moveto_pt(x_pt-0.5*size_pt, y_pt+0.5*size_pt),
3597 path.lineto_pt(x_pt+0.5*size_pt, y_pt-0.5*size_pt))
3599 def plus(self, x_pt, y_pt, size_pt):
3600 return (path.moveto_pt(x_pt-0.707106781*size_pt, y_pt),
3601 path.lineto_pt(x_pt+0.707106781*size_pt, y_pt),
3602 path.moveto_pt(x_pt, y_pt-0.707106781*size_pt),
3603 path.lineto_pt(x_pt, y_pt+0.707106781*size_pt))
3605 def square(self, x_pt, y_pt, size_pt):
3606 return (path.moveto_pt(x_pt-0.5*size_pt, y_pt-0.5*size_pt),
3607 path.lineto_pt(x_pt+0.5*size_pt, y_pt-0.5*size_pt),
3608 path.lineto_pt(x_pt+0.5*size_pt, y_pt+0.5*size_pt),
3609 path.lineto_pt(x_pt-0.5*size_pt, y_pt+0.5*size_pt),
3610 path.closepath())
3612 def triangle(self, x_pt, y_pt, size_pt):
3613 return (path.moveto_pt(x_pt-0.759835685*size_pt, y_pt-0.438691337*size_pt),
3614 path.lineto_pt(x_pt+0.759835685*size_pt, y_pt-0.438691337*size_pt),
3615 path.lineto_pt(x_pt, y_pt+0.877382675*size_pt),
3616 path.closepath())
3618 def circle(self, x_pt, y_pt, size_pt):
3619 return (path.arc_pt(x_pt, y_pt, 0.564189583*size_pt, 0, 360),
3620 path.closepath())
3622 def diamond(self, x_pt, y_pt, size_pt):
3623 return (path.moveto_pt(x_pt-0.537284965*size_pt, y_pt),
3624 path.lineto_pt(x_pt, y_pt-0.930604859*size_pt),
3625 path.lineto_pt(x_pt+0.537284965*size_pt, y_pt),
3626 path.lineto_pt(x_pt, y_pt+0.930604859*size_pt),
3627 path.closepath())
3629 changecross = attr.changelist([cross, plus, square, triangle, circle, diamond])
3630 changeplus = attr.changelist([plus, square, triangle, circle, diamond, cross])
3631 changesquare = attr.changelist([square, triangle, circle, diamond, cross, plus])
3632 changetriangle = attr.changelist([triangle, circle, diamond, cross, plus, square])
3633 changecircle = attr.changelist([circle, diamond, cross, plus, square, triangle])
3634 changediamond = attr.changelist([diamond, cross, plus, square, triangle, circle])
3635 changesquaretwice = attr.changelist([square, square, triangle, triangle, circle, circle, diamond, diamond])
3636 changetriangletwice = attr.changelist([triangle, triangle, circle, circle, diamond, diamond, square, square])
3637 changecircletwice = attr.changelist([circle, circle, diamond, diamond, square, square, triangle, triangle])
3638 changediamondtwice = attr.changelist([diamond, diamond, square, square, triangle, triangle, circle, circle])
3640 changestrokedfilled = attr.changelist([deco.stroked, deco.filled])
3641 changefilledstroked = attr.changelist([deco.filled, deco.stroked])
3643 changelinestyle = attr.changelist([style.linestyle.solid,
3644 style.linestyle.dashed,
3645 style.linestyle.dotted,
3646 style.linestyle.dashdotted])
3648 defaultsymbolattrs = [deco.stroked]
3649 defaulterrorbarattrs = []
3650 defaultlineattrs = [changelinestyle]
3652 def __init__(self, symbol=changecross,
3653 size="0.2 cm",
3654 errorscale=0.5,
3655 symbolattrs=[],
3656 errorbarattrs=[],
3657 lineattrs=[],
3658 epsilon=1e-10):
3659 self.size_str = size
3660 self.symbol = symbol
3661 self.errorscale = errorscale
3662 self.symbolattrs = symbolattrs
3663 self.errorbarattrs = errorbarattrs
3664 self.lineattrs = lineattrs
3665 self.epsilon = epsilon
3667 def setdata(self, graph, columns, data):
3669 - the instance should be considered read-only
3670 (it might be shared between several data)
3671 - data is the place where to store information
3672 - returns the dictionary of columns not used by the style"""
3674 # analyse column information
3675 data.index = {} # a nested index dictionary containing
3676 # column numbers, e.g. data.index["x"]["x"],
3677 # data.index["y"]["dmin"] etc.; the first key is a axis
3678 # name (without the axis number), the second is one of
3679 # the datanames ["x", "min", "max", "d", "dmin", "dmax"]
3680 data.axes = {} # mapping from axis name (without axis number) to the axis
3682 columns = columns.copy()
3683 for axisname in graph.axisnames:
3684 for dataname, pattern in [("x", re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % axisname)),
3685 ("min", re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % axisname)),
3686 ("max", re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % axisname)),
3687 ("d", re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % axisname)),
3688 ("dmin", re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % axisname)),
3689 ("dmax", re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % axisname))]:
3690 matchresult = self.setdatapattern(graph, columns, pattern)
3691 if matchresult is not None:
3692 axis, index = matchresult
3693 if data.axes.has_key(axisname):
3694 if data.axes[axisname] != axis:
3695 raise ValueError("axis mismatch for axis name '%s'" % axisname)
3696 data.index[axisname][dataname] = index
3697 else:
3698 data.index[axisname] = {dataname: index}
3699 data.axes[axisname] = axis
3700 if not data.axes.has_key(axisname):
3701 raise ValueError("missing columns for axis name '%s'" % axisname)
3702 if ((data.index[axisname].has_key("min") and data.index[axisname].has_key("d")) or
3703 (data.index[axisname].has_key("min") and data.index[axisname].has_key("dmin")) or
3704 (data.index[axisname].has_key("d") and data.index[axisname].has_key("dmin")) or
3705 (data.index[axisname].has_key("max") and data.index[axisname].has_key("d")) or
3706 (data.index[axisname].has_key("max") and data.index[axisname].has_key("dmax")) or
3707 (data.index[axisname].has_key("d") and data.index[axisname].has_key("dmax"))):
3708 raise ValueError("multiple errorbar definition for axis name '%s'" % axisname)
3709 if (not data.index[axisname].has_key("x") and
3710 (data.index[axisname].has_key("d") or
3711 data.index[axisname].has_key("dmin") or
3712 data.index[axisname].has_key("dmax"))):
3713 raise ValueError("errorbar definition start value missing for axis name '%s'" % axisname)
3714 return columns
3716 def selectstyle(self, selectindex, selecttotal, data):
3717 data.symbol = attr.selectattr(self.symbol, selectindex, selecttotal)
3718 data.size_pt = unit.topt(unit.length(attr.selectattr(self.size_str, selectindex, selecttotal), default_type="v"))
3719 data.errorsize_pt = self.errorscale * data.size_pt
3720 if self.symbolattrs is not None:
3721 data.symbolattrs = attr.selectattrs(self.defaultsymbolattrs + self.symbolattrs, selectindex, selecttotal)
3722 else:
3723 data.symbolattrs = None
3724 if self.errorbarattrs is not None:
3725 data.errorbarattrs = attr.selectattrs(self.defaulterrorbarattrs + self.errorbarattrs, selectindex, selecttotal)
3726 else:
3727 data.errorbarattrs = None
3728 if self.lineattrs is not None:
3729 data.lineattrs = attr.selectattrs(self.defaultlineattrs + self.lineattrs, selectindex, selecttotal)
3730 else:
3731 data.lineattrs = None
3733 def adjustaxes(self, columns, data):
3734 # reverse lookup for axisnames
3735 # TODO: the reverse lookup is ugly
3736 axisnames = []
3737 for column in columns:
3738 for axisname in data.index.keys():
3739 for thiscolumn in data.index[axisname].values():
3740 if thiscolumn == column and axisname not in axisnames:
3741 axisnames.append(axisname)
3742 # TODO: perform check to verify that all columns for a given axisname are available at the same time
3743 for axisname in axisnames:
3744 if data.index[axisname].has_key("x"):
3745 data.axes[axisname].adjustrange(data.points, data.index[axisname]["x"])
3746 if data.index[axisname].has_key("min"):
3747 data.axes[axisname].adjustrange(data.points, data.index[axisname]["min"])
3748 if data.index[axisname].has_key("max"):
3749 data.axes[axisname].adjustrange(data.points, data.index[axisname]["max"])
3750 if data.index[axisname].has_key("d"):
3751 data.axes[axisname].adjustrange(data.points, data.index[axisname]["x"], deltaindex=data.index[axisname]["d"])
3752 if data.index[axisname].has_key("dmin"):
3753 data.axes[axisname].adjustrange(data.points, data.index[axisname]["x"], deltaminindex=data.index[axisname]["dmin"])
3754 if data.index[axisname].has_key("dmax"):
3755 data.axes[axisname].adjustrange(data.points, data.index[axisname]["x"], deltamaxindex=data.index[axisname]["dmax"])
3757 def drawsymbol_pt(self, c, x_pt, y_pt, data, point=None):
3758 if data.symbolattrs is not None:
3759 c.draw(path.path(*data.symbol(self, x_pt, y_pt, data.size_pt)), data.symbolattrs)
3761 def drawpoints(self, graph, data):
3762 if data.lineattrs is not None:
3763 # TODO: bbox shortcut
3764 linecanvas = graph.insert(canvas.canvas())
3765 if data.errorbarattrs is not None:
3766 # TODO: bbox shortcut
3767 errorbarcanvas = graph.insert(canvas.canvas())
3768 data.path = path.path()
3769 linebasepoints = []
3770 lastvpos = None
3771 errorlist = []
3772 if data.errorbarattrs is not None:
3773 for axisname, axisindex in zip(graph.axisnames, xrange(sys.maxint)):
3774 if data.index[axisname].keys() != ["x"]:
3775 errorlist.append((axisname, axisindex))
3777 for point in data.points:
3778 # calculate vpos
3779 vpos = [] # list containing the graph coordinates of the point
3780 validvpos = 1 # valid position (but might be outside of the graph)
3781 drawsymbol = 1 # valid position inside the graph
3782 for axisname in graph.axisnames:
3783 try:
3784 v = data.axes[axisname].convert(point[data.index[axisname]["x"]])
3785 except:
3786 validvpos = 0
3787 drawsymbol = 0
3788 vpos.append(None)
3789 else:
3790 if v < - self.epsilon or v > 1 + self.epsilon:
3791 drawsymbol = 0
3792 vpos.append(v)
3794 # draw symbol
3795 if drawsymbol:
3796 xpos, ypos = graph.vpos_pt(*vpos)
3797 self.drawsymbol_pt(graph, xpos, ypos, data, point=point)
3799 # append linebasepoints
3800 if validvpos:
3801 if len(linebasepoints):
3802 # the last point was inside the graph
3803 if drawsymbol:
3804 linebasepoints.append((xpos, ypos))
3805 else:
3806 # cut end
3807 cut = 1
3808 for vstart, vend in zip(lastvpos, vpos):
3809 newcut = None
3810 if vend > 1:
3811 # 1 = vstart + (vend - vstart) * cut
3812 newcut = (1 - vstart)/(vend - vstart)
3813 if vend < 0:
3814 # 0 = vstart + (vend - vstart) * cut
3815 newcut = - vstart/(vend - vstart)
3816 if newcut is not None and newcut < cut:
3817 cut = newcut
3818 cutvpos = []
3819 for vstart, vend in zip(lastvpos, vpos):
3820 cutvpos.append(vstart + (vend - vstart) * cut)
3821 linebasepoints.append(graph.vpos_pt(*cutvpos))
3822 validvpos = 0 # clear linebasepoints below
3823 else:
3824 # the last point was outside the graph
3825 if lastvpos is not None:
3826 if drawsymbol:
3827 # cut beginning
3828 cut = 0
3829 for vstart, vend in zip(lastvpos, vpos):
3830 newcut = None
3831 if vstart > 1:
3832 # 1 = vstart + (vend - vstart) * cut
3833 newcut = (1 - vstart)/(vend - vstart)
3834 if vstart < 0:
3835 # 0 = vstart + (vend - vstart) * cut
3836 newcut = - vstart/(vend - vstart)
3837 if newcut is not None and newcut > cut:
3838 cut = newcut
3839 cutvpos = []
3840 for vstart, vend in zip(lastvpos, vpos):
3841 cutvpos.append(vstart + (vend - vstart) * cut)
3842 linebasepoints.append(graph.vpos_pt(*cutvpos))
3843 linebasepoints.append(graph.vpos_pt(*vpos))
3844 else:
3845 # sometimes cut beginning and end
3846 cutfrom = 0
3847 cutto = 1
3848 for vstart, vend in zip(lastvpos, vpos):
3849 newcutfrom = None
3850 if vstart > 1:
3851 # 1 = vstart + (vend - vstart) * cutfrom
3852 newcutfrom = (1 - vstart)/(vend - vstart)
3853 if vstart < 0:
3854 # 0 = vstart + (vend - vstart) * cutfrom
3855 newcutfrom = - vstart/(vend - vstart)
3856 if newcutfrom is not None and newcutfrom > cutfrom:
3857 cutfrom = newcutfrom
3858 newcutto = None
3859 if vend > 1:
3860 # 1 = vstart + (vend - vstart) * cutto
3861 newcutto = (1 - vstart)/(vend - vstart)
3862 if vend < 0:
3863 # 0 = vstart + (vend - vstart) * cutto
3864 newcutto = - vstart/(vend - vstart)
3865 if newcutto is not None and newcutto < cutto:
3866 cutto = newcutto
3867 if cutfrom < cutto:
3868 cutfromvpos = []
3869 cuttovpos = []
3870 for vstart, vend in zip(lastvpos, vpos):
3871 cutfromvpos.append(vstart + (vend - vstart) * cutfrom)
3872 cuttovpos.append(vstart + (vend - vstart) * cutto)
3873 linebasepoints.append(graph.vpos_pt(*cutfromvpos))
3874 linebasepoints.append(graph.vpos_pt(*cuttovpos))
3875 validvpos = 0 # clear linebasepoints below
3876 lastvpos = vpos
3877 else:
3878 lastvpos = None
3880 if not validvpos:
3881 # add baselinepoints to data.path
3882 if len(linebasepoints) > 1:
3883 data.path.append(path.moveto_pt(*linebasepoints[0]))
3884 if len(linebasepoints) > 2:
3885 data.path.append(path.multilineto_pt(linebasepoints[1:]))
3886 else:
3887 data.path.append(path.lineto_pt(*linebasepoints[1]))
3888 linebasepoints = []
3890 # errorbar loop over the different direction having errorbars
3891 for erroraxisname, erroraxisindex in errorlist:
3893 # check for validity of other point components
3894 for v, i in zip(vpos, xrange(sys.maxint)):
3895 if v is None and i != erroraxisindex:
3896 break
3897 else:
3898 # calculate min and max
3899 errorindex = data.index[erroraxisname]
3900 try:
3901 min = point[errorindex["x"]] - point[errorindex["d"]]
3902 except:
3903 try:
3904 min = point[errorindex["x"]] - point[errorindex["dmin"]]
3905 except:
3906 try:
3907 min = point[errorindex["min"]]
3908 except:
3909 min = None
3910 try:
3911 max = point[errorindex["x"]] + point[errorindex["d"]]
3912 except:
3913 try:
3914 max = point[errorindex["x"]] + point[errorindex["dmax"]]
3915 except:
3916 try:
3917 max = point[errorindex["max"]]
3918 except:
3919 max = None
3921 # calculate vmin and vmax
3922 try:
3923 vmin = data.axes[erroraxisname].convert(min)
3924 except:
3925 vmin = None
3926 try:
3927 vmax = data.axes[erroraxisname].convert(max)
3928 except:
3929 vmax = None
3931 # create vminpos and vmaxpos
3932 vcaps = []
3933 if vmin is not None:
3934 vminpos = vpos[:]
3935 if vmin > - self.epsilon and vmin < 1 + self.epsilon:
3936 vminpos[erroraxisindex] = vmin
3937 vcaps.append(vminpos)
3938 else:
3939 vminpos[erroraxisindex] = 0
3940 elif vpos[erroraxisindex] is not None:
3941 vminpos = vpos
3942 else:
3943 break
3944 if vmax is not None:
3945 vmaxpos = vpos[:]
3946 if vmax > - self.epsilon and vmax < 1 + self.epsilon:
3947 vmaxpos[erroraxisindex] = vmax
3948 vcaps.append(vmaxpos)
3949 else:
3950 vmaxpos[erroraxisindex] = 1
3951 elif vpos[erroraxisindex] is not None:
3952 vmaxpos = vpos
3953 else:
3954 break
3956 # create path for errorbars
3957 errorpath = path.path()
3958 errorpath += graph.vgeodesic(*(vminpos + vmaxpos))
3959 for vcap in vcaps:
3960 for axisname in graph.axisnames:
3961 if axisname != erroraxisname:
3962 errorpath += graph.vcap_pt(axisname, data.errorsize_pt, *vcap)
3964 # stroke errorpath
3965 if len(errorpath.path):
3966 errorbarcanvas.stroke(errorpath, data.errorbarattrs)
3968 # add baselinepoints to data.path
3969 if len(linebasepoints) > 1:
3970 data.path.append(path.moveto_pt(*linebasepoints[0]))
3971 if len(linebasepoints) > 2:
3972 data.path.append(path.multilineto_pt(linebasepoints[1:]))
3973 else:
3974 data.path.append(path.lineto_pt(*linebasepoints[1]))
3976 # stroke data.path
3977 if data.lineattrs is not None:
3978 linecanvas.stroke(data.path, data.lineattrs)
3980 def key_pt(self, c, x_pt, y_pt, width_pt, height_pt, data):
3981 self.drawsymbol_pt(c, x_pt+0.5*width_pt, y_pt+0.5*height_pt, data)
3982 if data.lineattrs is not None:
3983 c.stroke(path.line_pt(x_pt, y_pt+0.5*height_pt, x_pt+width_pt, y_pt+0.5*height_pt), data.lineattrs)
3986 class line(symbolline):
3988 def __init__(self, lineattrs=[]):
3989 symbolline.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
3992 class symbol(symbolline):
3994 def __init__(self, **kwargs):
3995 symbolline.__init__(self, lineattrs=None, **kwargs)
3998 class text(symbol):
4000 defaulttextattrs = [textmodule.halign.center, textmodule.vshift.mathaxis]
4002 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=[], **kwargs):
4003 self.textdx_str = textdx
4004 self.textdy_str = textdy
4005 self.textattrs = textattrs
4006 symbol.__init__(self, **kwargs)
4008 def setdata(self, graph, columns, data):
4009 columns = columns.copy()
4010 data.textindex = columns["text"]
4011 del columns["text"]
4012 return symbol.setdata(self, graph, columns, data)
4014 def selectstyle(self, selectindex, selecttotal, data):
4015 if self.textattrs is not None:
4016 data.textattrs = attr.selectattrs(self.defaulttextattrs + self.textattrs, selectindex, selecttotal)
4017 else:
4018 data.textattrs = None
4019 symbol.selectstyle(self, selectindex, selecttotal, data)
4021 def drawsymbol_pt(self, c, x, y, data, point=None):
4022 symbol.drawsymbol_pt(self, c, x, y, data, point)
4023 if None not in (x, y, point[data.textindex]) and data.textattrs is not None:
4024 c.text_pt(x + data.textdx_pt, y + data.textdy_pt, str(point[data.textindex]), data.textattrs)
4026 def drawpoints(self, graph, points):
4027 data.textdx = unit.length(self.textdx_str, default_type="v")
4028 data.textdy = unit.length(self.textdy_str, default_type="v")
4029 data.textdx_pt = unit.topt(data.textdx)
4030 data.textdy_pt = unit.topt(data.textdy)
4031 symbol.drawpoints(self, graph, points)
4034 class arrow(_style):
4036 defaultlineattrs = []
4037 defaultarrowattrs = []
4039 def __init__(self, linelength="0.2 cm", arrowsize="0.1 cm", lineattrs=[], arrowattrs=[], epsilon=1e-10):
4040 self.linelength_str = linelength
4041 self.arrowsize_str = arrowsize
4042 self.lineattrs = lineattrs
4043 self.arrowattrs = arrowattrs
4044 self.epsilon = epsilon
4046 def setdata(self, graph, columns, data):
4047 if len(graph.axisnames) != 2:
4048 raise TypeError("arrow style restricted on two-dimensional graphs")
4049 columns = columns.copy()
4050 data.xaxis, data.xindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[0]))
4051 data.yaxis, data.yindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[1]))
4052 data.sizeindex = columns["size"]
4053 del columns["size"]
4054 data.angleindex = columns["angle"]
4055 del columns["angle"]
4056 return columns
4058 def adjustaxes(self, columns, data):
4059 if data.xindex in columns:
4060 data.xaxis.adjustrange(data.points, data.xindex)
4061 if data.yindex in columns:
4062 data.yaxis.adjustrange(data.points, data.yindex)
4064 def selectstyle(self, selectindex, selecttotal, data):
4065 if self.lineattrs is not None:
4066 data.lineattrs = attr.selectattrs(self.defaultlineattrs + self.lineattrs, selectindex, selecttotal)
4067 else:
4068 data.lineattrs = None
4069 if self.arrowattrs is not None:
4070 data.arrowattrs = attr.selectattrs(self.defaultarrowattrs + self.arrowattrs, selectindex, selecttotal)
4071 else:
4072 data.arrowattrs = None
4074 def drawpoints(self, graph, data):
4075 if data.lineattrs is not None and data.arrowattrs is not None:
4076 arrowsize = unit.length(self.arrowsize_str, default_type="v")
4077 linelength = unit.length(self.linelength_str, default_type="v")
4078 arrowsize_pt = unit.topt(arrowsize)
4079 linelength_pt = unit.topt(linelength)
4080 for point in data.points:
4081 xpos, ypos = graph.pos_pt(point[data.xindex], point[data.yindex], xaxis=data.xaxis, yaxis=data.yaxis)
4082 if point[data.sizeindex] > self.epsilon:
4083 dx = math.cos(point[data.angleindex]*math.pi/180.0)
4084 dy = math.sin(point[data.angleindex]*math.pi/180)
4085 x1 = xpos-0.5*dx*linelength_pt*point[data.sizeindex]
4086 y1 = ypos-0.5*dy*linelength_pt*point[data.sizeindex]
4087 x2 = xpos+0.5*dx*linelength_pt*point[data.sizeindex]
4088 y2 = ypos+0.5*dy*linelength_pt*point[data.sizeindex]
4089 graph.stroke(path.line_pt(x1, y1, x2, y2), data.lineattrs +
4090 [deco.earrow(data.arrowattrs, size=arrowsize*point[data.sizeindex])])
4093 class rect(_style):
4095 def __init__(self, palette=color.palette.Gray):
4096 self.palette = palette
4098 def setdata(self, graph, columns, data):
4099 if len(graph.axisnames) != 2:
4100 raise TypeError("arrow style restricted on two-dimensional graphs")
4101 columns = columns.copy()
4102 data.xaxis, data.xminindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.axisnames[0]))
4103 data.yaxis, data.yminindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.axisnames[1]))
4104 xaxis, data.xmaxindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.axisnames[0]))
4105 yaxis, data.ymaxindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.axisnames[1]))
4106 if xaxis != data.xaxis or yaxis != data.yaxis:
4107 raise ValueError("min/max values should use the same axes")
4108 data.colorindex = columns["color"]
4109 del columns["color"]
4110 return columns
4112 def selectstyle(self, selectindex, selecttotal, data):
4113 pass
4115 def adjustaxes(self, columns, data):
4116 if data.xminindex in columns:
4117 data.xaxis.adjustrange(data.points, data.xminindex)
4118 if data.xmaxindex in columns:
4119 data.xaxis.adjustrange(data.points, data.xmaxindex)
4120 if data.yminindex in columns:
4121 data.yaxis.adjustrange(data.points, data.yminindex)
4122 if data.ymaxindex in columns:
4123 data.yaxis.adjustrange(data.points, data.ymaxindex)
4125 def drawpoints(self, graph, data):
4126 # TODO: bbox shortcut
4127 c = graph.insert(canvas.canvas())
4128 lastcolorvalue = None
4129 for point in data.points:
4130 try:
4131 xvmin = data.xaxis.convert(point[data.xminindex])
4132 xvmax = data.xaxis.convert(point[data.xmaxindex])
4133 yvmin = data.yaxis.convert(point[data.yminindex])
4134 yvmax = data.yaxis.convert(point[data.ymaxindex])
4135 colorvalue = point[data.colorindex]
4136 if colorvalue != lastcolorvalue:
4137 color = self.palette.getcolor(point[data.colorindex])
4138 except:
4139 continue
4140 if ((xvmin < 0 and xvmax < 0) or (xvmin > 1 and xvmax > 1) or
4141 (yvmin < 0 and yvmax < 0) or (yvmin > 1 and yvmax > 1)):
4142 continue
4143 if xvmin < 0:
4144 xvmin = 0
4145 elif xvmin > 1:
4146 xvmin = 1
4147 if xvmax < 0:
4148 xvmax = 0
4149 elif xvmax > 1:
4150 xvmax = 1
4151 if yvmin < 0:
4152 yvmin = 0
4153 elif yvmin > 1:
4154 yvmin = 1
4155 if yvmax < 0:
4156 yvmax = 0
4157 elif yvmax > 1:
4158 yvmax = 1
4159 p = graph.vgeodesic(xvmin, yvmin, xvmax, yvmin)
4160 p.append(graph.vgeodesic_el(xvmax, yvmin, xvmax, yvmax))
4161 p.append(graph.vgeodesic_el(xvmax, yvmax, xvmin, yvmax))
4162 p.append(graph.vgeodesic_el(xvmin, yvmax, xvmin, yvmin))
4163 p.append(path.closepath())
4164 if colorvalue != lastcolorvalue:
4165 c.set([color])
4166 c.fill(p)
4168 class bar(_style):
4170 defaultfrompathattrs = []
4171 defaultbarattrs = [color.palette.Rainbow, deco.stroked([color.gray.black])]
4173 def __init__(self, fromvalue=None, frompathattrs=[], barattrs=[], subnames=None, epsilon=1e-10):
4174 self.fromvalue = fromvalue
4175 self.frompathattrs = frompathattrs
4176 self.barattrs = barattrs
4177 self.subnames = subnames
4178 self.epsilon = epsilon
4180 def setdata(self, graph, columns, data):
4181 # TODO: remove limitation to 2d graphs
4182 if len(graph.axisnames) != 2:
4183 raise TypeError("arrow style currently restricted on two-dimensional graphs")
4184 columns = columns.copy()
4185 xvalue = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[0]))
4186 yvalue = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.axisnames[1]))
4187 if (xvalue is None and yvalue is None) or (xvalue is not None and yvalue is not None):
4188 raise TypeError("must specify exactly one value axis")
4189 if xvalue is not None:
4190 data.valuepos = 0
4191 data.nameaxis, data.nameindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)name$" % graph.axisnames[1]))
4192 data.valueaxis = xvalue[0]
4193 data.valueindices = [xvalue[1]]
4194 else:
4195 data.valuepos = 1
4196 data.nameaxis, data.nameindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)name$" % graph.axisnames[0]))
4197 data.valueaxis = yvalue[0]
4198 data.valueindices = [yvalue[1]]
4199 for i in xrange(1, sys.maxint):
4200 try:
4201 valueaxis, valueindex = _style.setdatapattern(self, graph, columns, re.compile(r"(%s([2-9]|[1-9][0-9]+)?)stack%i$" % (graph.axisnames[data.valuepos], i)))
4202 except:
4203 break
4204 if data.valueaxis != valueaxis:
4205 raise ValueError("different value axes for stacked bars")
4206 data.valueindices.append(valueindex)
4207 return columns
4209 def selectstyle(self, selectindex, selecttotal, data):
4210 if selectindex:
4211 data.frompathattrs = None
4212 else:
4213 data.frompathattrs = self.defaultfrompathattrs + self.frompathattrs
4214 if selecttotal > 1:
4215 if self.barattrs is not None:
4216 data.barattrs = attr.selectattrs(self.defaultbarattrs + self.barattrs, selectindex, selecttotal)
4217 else:
4218 data.barattrs = None
4219 else:
4220 data.barattrs = self.defaultbarattrs + self.barattrs
4221 data.selectindex = selectindex
4222 data.selecttotal = selecttotal
4223 if data.selecttotal != 1 and self.subnames is not None:
4224 raise ValueError("subnames not allowed when iterating over bars")
4226 def adjustaxes(self, columns, data):
4227 if data.nameindex in columns:
4228 if data.selecttotal == 1:
4229 data.nameaxis.adjustrange(data.points, data.nameindex, subnames=self.subnames)
4230 else:
4231 for i in range(data.selecttotal):
4232 data.nameaxis.adjustrange(data.points, data.nameindex, subnames=[i])
4233 for valueindex in data.valueindices:
4234 if valueindex in columns:
4235 data.valueaxis.adjustrange(data.points, valueindex)
4237 def drawpoints(self, graph, data):
4238 if self.fromvalue is not None:
4239 vfromvalue = data.valueaxis.convert(self.fromvalue)
4240 if vfromvalue < -self.epsilon:
4241 vfromvalue = 0
4242 if vfromvalue > 1 + self.epsilon:
4243 vfromvalue = 1
4244 if data.frompathattrs is not None and vfromvalue > self.epsilon and vfromvalue < 1 - self.epsilon:
4245 if data.valuepos:
4246 p = graph.vgeodesic(0, vfromvalue, 1, vfromvalue)
4247 else:
4248 p = graph.vgeodesic(vfromvalue, 0, vfromvalue, 1)
4249 graph.stroke(p, data.frompathattrs)
4250 else:
4251 vfromvalue = 0
4252 l = len(data.valueindices)
4253 if l > 1:
4254 barattrslist = []
4255 for i in range(l):
4256 barattrslist.append(attr.selectattrs(data.barattrs, i, l))
4257 else:
4258 barattrslist = [data.barattrs]
4259 for point in data.points:
4260 vvaluemax = vfromvalue
4261 for valueindex, barattrs in zip(data.valueindices, barattrslist):
4262 vvaluemin = vvaluemax
4263 try:
4264 vvaluemax = data.valueaxis.convert(point[valueindex])
4265 except:
4266 continue
4268 if data.selecttotal == 1:
4269 try:
4270 vnamemin = data.nameaxis.convert((point[data.nameindex], 0))
4271 except:
4272 continue
4273 try:
4274 vnamemax = data.nameaxis.convert((point[data.nameindex], 1))
4275 except:
4276 continue
4277 else:
4278 try:
4279 vnamemin = data.nameaxis.convert((point[data.nameindex], data.selectindex, 0))
4280 except:
4281 continue
4282 try:
4283 vnamemax = data.nameaxis.convert((point[data.nameindex], data.selectindex, 1))
4284 except:
4285 continue
4287 if data.valuepos:
4288 p = graph.vgeodesic(vnamemin, vvaluemin, vnamemin, vvaluemax)
4289 p.append(graph.vgeodesic_el(vnamemin, vvaluemax, vnamemax, vvaluemax))
4290 p.append(graph.vgeodesic_el(vnamemax, vvaluemax, vnamemax, vvaluemin))
4291 p.append(graph.vgeodesic_el(vnamemax, vvaluemin, vnamemin, vvaluemin))
4292 p.append(path.closepath())
4293 else:
4294 p = graph.vgeodesic(vvaluemin, vnamemin, vvaluemin, vnamemax)
4295 p.append(graph.vgeodesic_el(vvaluemin, vnamemax, vvaluemax, vnamemax))
4296 p.append(graph.vgeodesic_el(vvaluemax, vnamemax, vvaluemax, vnamemin))
4297 p.append(graph.vgeodesic_el(vvaluemax, vnamemin, vvaluemin, vnamemin))
4298 p.append(path.closepath())
4299 if barattrs is not None:
4300 graph.fill(p, barattrs)
4302 def key_pt(self, c, x_pt, y_pt, width_pt, height_pt, data):
4303 l = len(data.valueindices)
4304 if l > 1:
4305 for i in range(l):
4306 c.fill(path.rect_pt(x_pt+i*width_pt/l, y_pt, width_pt/l, height_pt), attr.selectattrs(data.barattrs, i, l))
4307 else:
4308 c.fill(path.rect_pt(x_pt, y_pt, width_pt, height_pt), data.barattrs)
4312 ################################################################################
4313 # data
4314 ################################################################################
4317 class data:
4319 defaultstyle = symbol()
4321 def __init__(self, file, title=helper.nodefault, context={}, **columns):
4322 self.title = title
4323 if helper.isstring(file):
4324 self.data = datamodule.datafile(file)
4325 else:
4326 self.data = file
4327 if title is helper.nodefault:
4328 self.title = "(unknown)"
4329 else:
4330 self.title = title
4331 self.columns = {}
4332 for key, column in columns.items():
4333 try:
4334 self.columns[key] = self.data.getcolumnno(column)
4335 except datamodule.ColumnError:
4336 self.columns[key] = len(self.data.titles)
4337 self.data.addcolumn(column, context=context)
4338 self.points = self.data.data
4340 def setstyle(self, graph, style):
4341 self.style = style
4342 unhandledcolumns = self.style.setdata(graph, self.columns, self)
4343 unhandledcolumnkeys = unhandledcolumns.keys()
4344 if len(unhandledcolumnkeys):
4345 raise ValueError("style couldn't handle column keys %s" % unhandledcolumnkeys)
4347 def selectstyle(self, graph, selectindex, selecttotal):
4348 self.style.selectstyle(selectindex, selecttotal, self)
4350 def adjustaxes(self, graph, step):
4352 - on step == 0 axes with fixed data should be adjusted
4353 - on step == 1 the current axes ranges might be used to
4354 calculate further data (e.g. y data for a function y=f(x)
4355 where the y range depends on the x range)
4356 - on step == 2 axes ranges not previously set should be
4357 updated by data accumulated by step 1"""
4358 if step == 0:
4359 self.style.adjustaxes(self.columns.values(), self)
4361 def draw(self, graph):
4362 self.style.drawpoints(graph, self)
4365 class function:
4367 defaultstyle = line()
4369 def __init__(self, expression, title=helper.nodefault, min=None, max=None, points=100, parser=mathtree.parser(), context={}):
4370 if title is helper.nodefault:
4371 self.title = expression
4372 else:
4373 self.title = title
4374 self.min = min
4375 self.max = max
4376 self.nopoints = points
4377 self.context = context
4378 self.result, expression = [x.strip() for x in expression.split("=")]
4379 self.mathtree = parser.parse(expression)
4380 self.variable = None
4382 def setstyle(self, graph, style):
4383 self.style = style
4384 for variable in self.mathtree.VarList():
4385 if variable in graph.axes.keys():
4386 if self.variable is None:
4387 self.variable = variable
4388 else:
4389 raise ValueError("multiple variables found")
4390 if self.variable is None:
4391 raise ValueError("no variable found")
4392 self.xaxis = graph.axes[self.variable]
4393 unhandledcolumns = self.style.setdata(graph, {self.variable: 0, self.result: 1}, self)
4394 unhandledcolumnkeys = unhandledcolumns.keys()
4395 if len(unhandledcolumnkeys):
4396 raise ValueError("style couldn't handle column keys %s" % unhandledcolumnkeys)
4398 def selectstyle(self, graph, selectindex, selecttotal):
4399 self.style.selectstyle(selectindex, selecttotal, self)
4401 def adjustaxes(self, graph, step):
4403 - on step == 0 axes with fixed data should be adjusted
4404 - on step == 1 the current axes ranges might be used to
4405 calculate further data (e.g. y data for a function y=f(x)
4406 where the y range depends on the x range)
4407 - on step == 2 axes ranges not previously set should be
4408 updated by data accumulated by step 1"""
4409 if step == 0:
4410 min, max = graph.axes[self.variable].getrange()
4411 if self.min is not None: min = self.min
4412 if self.max is not None: max = self.max
4413 vmin = self.xaxis.convert(min)
4414 vmax = self.xaxis.convert(max)
4415 self.points = []
4416 for i in range(self.nopoints):
4417 x = self.xaxis.invert(vmin + (vmax-vmin)*i / (self.nopoints-1.0))
4418 self.points.append([x])
4419 self.style.adjustaxes([0], self)
4420 elif step == 1:
4421 for point in self.points:
4422 self.context[self.variable] = point[0]
4423 try:
4424 point.append(self.mathtree.Calc(**self.context))
4425 except (ArithmeticError, ValueError):
4426 point.append(None)
4427 elif step == 2:
4428 self.style.adjustaxes([1], self)
4430 def draw(self, graph):
4431 self.style.drawpoints(graph, self)
4434 class paramfunction:
4436 defaultstyle = line()
4438 def __init__(self, varname, min, max, expression, title=helper.nodefault, points=100, parser=mathtree.parser(), context={}):
4439 if title is helper.nodefault:
4440 self.title = expression
4441 else:
4442 self.title = title
4443 self.varname = varname
4444 self.min = min
4445 self.max = max
4446 self.nopoints = points
4447 self.expression = {}
4448 self.mathtrees = {}
4449 varlist, expressionlist = expression.split("=")
4450 if mathtree.__useparser__ == mathtree.__newparser__: # XXX: switch between mathtree-parsers
4451 keys = varlist.split(",")
4452 mtrees = helper.ensurelist(parser.parse(expressionlist))
4453 if len(keys) != len(mtrees):
4454 raise ValueError("unpack tuple of wrong size")
4455 for i in range(len(keys)):
4456 key = keys[i].strip()
4457 if self.mathtrees.has_key(key):
4458 raise ValueError("multiple assignment in tuple")
4459 self.mathtrees[key] = mtrees[i]
4460 if len(keys) != len(self.mathtrees.keys()):
4461 raise ValueError("unpack tuple of wrong size")
4462 else:
4463 parsestr = mathtree.ParseStr(expressionlist)
4464 for key in varlist.split(","):
4465 key = key.strip()
4466 if self.mathtrees.has_key(key):
4467 raise ValueError("multiple assignment in tuple")
4468 try:
4469 self.mathtrees[key] = parser.ParseMathTree(parsestr)
4470 break
4471 except mathtree.CommaFoundMathTreeParseError, e:
4472 self.mathtrees[key] = e.MathTree
4473 else:
4474 raise ValueError("unpack tuple of wrong size")
4475 if len(varlist.split(",")) != len(self.mathtrees.keys()):
4476 raise ValueError("unpack tuple of wrong size")
4477 self.points = []
4478 for i in range(self.nopoints):
4479 context[self.varname] = self.min + (self.max-self.min)*i / (self.nopoints-1.0)
4480 line = []
4481 for key, tree in self.mathtrees.items():
4482 line.append(tree.Calc(**context))
4483 self.points.append(line)
4485 def setstyle(self, graph, style):
4486 self.style = style
4487 columns = {}
4488 for key, index in zip(self.mathtrees.keys(), xrange(sys.maxint)):
4489 columns[key] = index
4490 unhandledcolumns = self.style.setdata(graph, columns, self)
4491 unhandledcolumnkeys = unhandledcolumns.keys()
4492 if len(unhandledcolumnkeys):
4493 raise ValueError("style couldn't handle column keys %s" % unhandledcolumnkeys)
4495 def selectstyle(self, graph, selectindex, selecttotal):
4496 self.style.selectstyle(selectindex, selecttotal, self)
4498 def adjustaxes(self, graph, step):
4499 if step == 0:
4500 self.style.adjustaxes(list(range(len(self.mathtrees.items()))), self)
4502 def draw(self, graph):
4503 self.style.drawpoints(graph, self)