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