cleantmp improvements; manualpart and partitioners mix keyword removed
[PyX.git] / pyx / graph.py
blob9abf06c87b83ca7a81141e2f3db5c00a076c2f81
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 import re, math, string, sys
26 import bbox, box, canvas, path, unit, mathtree, color, helper
27 import text as textmodule
28 import data as datamodule
29 import trafo as trafomodule
32 goldenmean = 0.5 * (math.sqrt(5) + 1)
35 ################################################################################
36 # maps
37 ################################################################################
39 class _Imap:
40 """interface definition of a map
41 maps convert a value into another value by bijective transformation f"""
43 def convert(self, x):
44 "returns f(x)"
46 def invert(self, y):
47 "returns f^-1(y) where f^-1 is the inverse transformation (x=f^-1(f(x)) for all x)"
49 def setbasepoints(self, basepoints):
50 """set basepoints for the convertions
51 basepoints are tuples (x, y) with y == f(x) and x == f^-1(y)
52 the number of basepoints needed might depend on the transformation
53 usually two pairs are needed like for linear maps, logarithmic maps, etc."""
56 class _linmap:
57 "linear mapping"
59 __implements__ = _Imap
61 def setbasepoints(self, basepoints):
62 self.dydx = (basepoints[1][1] - basepoints[0][1]) / float(basepoints[1][0] - basepoints[0][0])
63 self.dxdy = (basepoints[1][0] - basepoints[0][0]) / float(basepoints[1][1] - basepoints[0][1])
64 self.x1 = basepoints[0][0]
65 self.y1 = basepoints[0][1]
66 return self
68 def convert(self, value):
69 return self.y1 + self.dydx * (value - self.x1)
71 def invert(self, value):
72 return self.x1 + self.dxdy * (value - self.y1)
75 class _logmap:
76 "logarithmic mapping"
77 __implements__ = _Imap
79 def setbasepoints(self, basepoints):
80 self.dydx = ((basepoints[1][1] - basepoints[0][1]) /
81 float(math.log(basepoints[1][0]) - math.log(basepoints[0][0])))
82 self.dxdy = ((math.log(basepoints[1][0]) - math.log(basepoints[0][0])) /
83 float(basepoints[1][1] - basepoints[0][1]))
84 self.x1 = math.log(basepoints[0][0])
85 self.y1 = basepoints[0][1]
86 return self
88 def convert(self, value):
89 return self.y1 + self.dydx * (math.log(value) - self.x1)
91 def invert(self, value):
92 return math.exp(self.x1 + self.dxdy * (value - self.y1))
96 ################################################################################
97 # partitioner
98 # please note the nomenclature:
99 # - a partition is a list of tick instances; to reduce name clashes, a
100 # partition is called ticks
101 # - a partitioner is a class creating a single or several ticks
102 # - an axis has a part attribute where it stores a partitioner or/and some
103 # (manually set) ticks -> the part attribute is used to create the ticks
104 # in the axis finish method
105 ################################################################################
108 class frac:
109 """fraction class for rational arithmetics
110 the axis partitioning uses rational arithmetics (with infinite accuracy)
111 basically it contains self.enum and self.denom"""
113 def stringfrac(self, s):
114 "converts a string 0.123 into a frac"
115 expparts = s.split("e")
116 if len(expparts) > 2:
117 raise ValueError("multiple 'e' found in '%s'" % s)
118 commaparts = expparts[0].split(".")
119 if len(commaparts) > 2:
120 raise ValueError("multiple '.' found in '%s'" % expparts[0])
121 if len(commaparts) == 1:
122 commaparts = [commaparts[0], ""]
123 result = frac((1, 10l), power=len(commaparts[1]))
124 neg = len(commaparts[0]) and commaparts[0][0] == "-"
125 if neg:
126 commaparts[0] = commaparts[0][1:]
127 elif len(commaparts[0]) and commaparts[0][0] == "+":
128 commaparts[0] = commaparts[0][1:]
129 if len(commaparts[0]):
130 if not commaparts[0].isdigit():
131 raise ValueError("unrecognized characters in '%s'" % s)
132 x = long(commaparts[0])
133 else:
134 x = 0
135 if len(commaparts[1]):
136 if not commaparts[1].isdigit():
137 raise ValueError("unrecognized characters in '%s'" % s)
138 y = long(commaparts[1])
139 else:
140 y = 0
141 result.enum = x*result.denom+y
142 if neg:
143 result.enum = -result.enum
144 if len(expparts) == 2:
145 neg = expparts[1][0] == "-"
146 if neg:
147 expparts[1] = expparts[1][1:]
148 elif expparts[1][0] == "+":
149 expparts[1] = expparts[1][1:]
150 if not expparts[1].isdigit():
151 raise ValueError("unrecognized characters in '%s'" % s)
152 if neg:
153 result *= frac((1, 10l), power=long(expparts[1]))
154 else:
155 result *= frac((10, 1l), power=long(expparts[1]))
156 return result
158 def floatfrac(self, x, floatprecision):
159 "converts a float into a frac with finite resolution"
160 if helper.isinteger(floatprecision) and floatprecision < 0:
161 # this would be extremly vulnerable
162 raise RuntimeError("float resolution must be non-negative integer")
163 return self.stringfrac(("%%.%ig" % floatprecision) % x)
165 def __init__(self, x, power=None, floatprecision=10):
166 "for power!=None: frac=(enum/denom)**power"
167 if helper.isnumber(x):
168 value = self.floatfrac(x, floatprecision)
169 enum, denom = value.enum, value.denom
170 elif helper.isstring(x):
171 fraction = x.split("/")
172 if len(fraction) > 2:
173 raise ValueError("multiple '/' found in '%s'" % x)
174 value = self.stringfrac(fraction[0])
175 if len(fraction) == 2:
176 value2 = self.stringfrac(fraction[1])
177 value = value / value2
178 enum, denom = value.enum, value.denom
179 else:
180 try:
181 enum, denom = x
182 except (TypeError, AttributeError):
183 enum, denom = x.enum, x.denom
184 if not helper.isinteger(enum) or not helper.isinteger(denom): raise TypeError("integer type expected")
185 if not denom: raise ZeroDivisionError("zero denominator")
186 if power != None:
187 if not helper.isinteger(power): raise TypeError("integer type expected")
188 if power >= 0:
189 self.enum = long(enum) ** power
190 self.denom = long(denom) ** power
191 else:
192 self.enum = long(denom) ** (-power)
193 self.denom = long(enum) ** (-power)
194 else:
195 self.enum = enum
196 self.denom = denom
198 def __cmp__(self, other):
199 if other is None:
200 return 1
201 return cmp(self.enum * other.denom, other.enum * self.denom)
203 def __abs__(self):
204 return frac((abs(self.enum), abs(self.denom)))
206 def __mul__(self, other):
207 return frac((self.enum * other.enum, self.denom * other.denom))
209 def __div__(self, other):
210 return frac((self.enum * other.denom, self.denom * other.enum))
212 def __float__(self):
213 "caution: avoid final precision of floats"
214 return float(self.enum) / self.denom
216 def __str__(self):
217 return "%i/%i" % (self.enum, self.denom)
220 class tick(frac):
221 """tick class
222 a tick is a frac enhanced by
223 - self.ticklevel (0 = tick, 1 = subtick, etc.)
224 - self.labellevel (0 = label, 1 = sublabel, etc.)
225 - self.label (a string) and self.labelattrs (a list, defaults to [])
226 When ticklevel or labellevel is None, no tick or label is present at that value.
227 When label is None, it should be automatically created (and stored), once the
228 an axis painter needs it. Classes, which implement _Itexter do precisely that."""
230 def __init__(self, pos, ticklevel=0, labellevel=0, label=None, labelattrs=[], **kwargs):
231 """initializes the instance
232 - see class description for the parameter description
233 - **kwargs are passed to the frac constructor"""
234 frac.__init__(self, pos, **kwargs)
235 self.ticklevel = ticklevel
236 self.labellevel = labellevel
237 self.label = label
238 self.labelattrs = helper.ensurelist(labelattrs)[:]
240 def merge(self, other):
241 """merges two ticks together:
242 - the lower ticklevel/labellevel wins
243 - the label is *never* taken over from other
244 - the ticks should be at the same position (otherwise it doesn't make sense)
245 -> this is NOT checked"""
246 if self.ticklevel is None or (other.ticklevel is not None and other.ticklevel < self.ticklevel):
247 self.ticklevel = other.ticklevel
248 if self.labellevel is None or (other.labellevel is not None and other.labellevel < self.labellevel):
249 self.labellevel = other.labellevel
252 def _mergeticklists(list1, list2):
253 """helper function to merge tick lists
254 - return a merged list of ticks out of list1 and list2
255 - CAUTION: original lists have to be ordered
256 (the returned list is also ordered)
257 - CAUTION: original lists are modified and they share references to
258 the result list!"""
259 # TODO: improve this using bisect?!
260 if list1 is None: return list2
261 if list2 is None: return list1
262 i = 0
263 j = 0
264 try:
265 while 1: # we keep on going until we reach an index error
266 while list2[j] < list1[i]: # insert tick
267 list1.insert(i, list2[j])
268 i += 1
269 j += 1
270 if list2[j] == list1[i]: # merge tick
271 list1[i].merge(list2[j])
272 j += 1
273 i += 1
274 except IndexError:
275 if j < len(list2):
276 list1 += list2[j:]
277 return list1
280 def _mergelabels(ticks, labels):
281 """helper function to merge labels into ticks
282 - when labels is not None, the label of all ticks with
283 labellevel different from None are set
284 - labels need to be a list of lists of strings,
285 where the first list contain the strings to be
286 used as labels for the ticks with labellevel 0,
287 the second list for labellevel 1, etc.
288 - when the maximum labellevel is 0, just a list of
289 strings might be provided as the labels argument
290 - IndexError is raised, when a list length doesn't match"""
291 if helper.issequenceofsequences(labels):
292 for label, level in zip(labels, xrange(sys.maxint)):
293 usetext = helper.ensuresequence(label)
294 i = 0
295 for tick in ticks:
296 if tick.labellevel == level:
297 tick.label = usetext[i]
298 i += 1
299 if i != len(usetext):
300 raise IndexError("wrong list length of labels at level %i" % level)
301 elif labels is not None:
302 usetext = helper.ensuresequence(labels)
303 i = 0
304 for tick in ticks:
305 if tick.labellevel == 0:
306 tick.label = usetext[i]
307 i += 1
308 if i != len(usetext):
309 raise IndexError("wrong list length of labels")
312 class _Ipart:
313 """interface definition of a partition scheme
314 partition schemes are used to create a list of ticks"""
316 def defaultpart(self, min, max, extendmin, extendmax):
317 """create a partition
318 - returns an ordered list of ticks for the interval min to max
319 - the interval is given in float numbers, thus an appropriate
320 conversion to rational numbers has to be performed
321 - extendmin and extendmax are booleans (integers)
322 - when extendmin or extendmax is set, the ticks might
323 extend the min-max range towards lower and higher
324 ranges, respectively"""
326 def lesspart(self):
327 """create another partition which contains less ticks
328 - this method is called several times after a call of defaultpart
329 - returns an ordered list of ticks with less ticks compared to
330 the partition returned by defaultpart and by previous calls
331 of lesspart
332 - the creation of a partition with strictly *less* ticks
333 is not to be taken serious
334 - the method might return None, when no other appropriate
335 partition can be created"""
338 def morepart(self):
339 """create another partition which contains more ticks
340 see lesspart, but increase the number of ticks"""
343 class linpart:
344 """linear partition scheme
345 ticks and label distances are explicitly provided to the constructor"""
347 __implements__ = _Ipart
349 def __init__(self, tickdist=None, labeldist=None, labels=None, extendtick=0, extendlabel=None, epsilon=1e-10):
350 """configuration of the partition scheme
351 - tickdist and labeldist should be a list, where the first value
352 is the distance between ticks with ticklevel/labellevel 0,
353 the second list for ticklevel/labellevel 1, etc.;
354 a single entry is allowed without being a list
355 - tickdist and labeldist values are passed to the frac constructor
356 - when labeldist is None and tickdist is not None, the tick entries
357 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
358 - labels are applied to the resulting partition via the
359 mergelabels function (additional information available there)
360 - extendtick allows for the extension of the range given to the
361 defaultpart method to include the next tick with the specified
362 level (None turns off this feature); note, that this feature is
363 also disabled, when an axis prohibits its range extension by
364 the extendmin/extendmax variables given to the defaultpart method
365 - extendlabel is analogous to extendtick, but for labels
366 - epsilon allows for exceeding the axis range by this relative
367 value (relative to the axis range given to the defaultpart method)
368 without creating another tick specified by extendtick/extendlabel"""
369 if tickdist is None and labeldist is not None:
370 self.ticklist = (frac(helper.ensuresequence(labeldist)[0]),)
371 else:
372 self.ticklist = map(frac, helper.ensuresequence(tickdist))
373 if labeldist is None and tickdist is not None:
374 self.labellist = (frac(helper.ensuresequence(tickdist)[0]),)
375 else:
376 self.labellist = map(frac, helper.ensuresequence(labeldist))
377 self.labels = labels
378 self.extendtick = extendtick
379 self.extendlabel = extendlabel
380 self.epsilon = epsilon
382 def extendminmax(self, min, max, frac, extendmin, extendmax):
383 """return new min, max tuple extending the range min, max
384 - frac is the tick distance to be used
385 - extendmin and extendmax are booleans to allow for the extension"""
386 if extendmin:
387 min = float(frac) * math.floor(min / float(frac) + self.epsilon)
388 if extendmax:
389 max = float(frac) * math.ceil(max / float(frac) - self.epsilon)
390 return min, max
392 def getticks(self, min, max, frac, ticklevel=None, labellevel=None):
393 """return a list of equal spaced ticks
394 - the tick distance is frac, the ticklevel is set to ticklevel and
395 the labellevel is set to labellevel
396 - min, max is the range where ticks should be placed"""
397 imin = int(math.ceil(min / float(frac) - 0.5 * self.epsilon))
398 imax = int(math.floor(max / float(frac) + 0.5 * self.epsilon))
399 ticks = []
400 for i in range(imin, imax + 1):
401 ticks.append(tick((long(i) * frac.enum, frac.denom), ticklevel=ticklevel, labellevel=labellevel))
402 return ticks
404 def defaultpart(self, min, max, extendmin, extendmax):
405 if self.extendtick is not None and len(self.ticklist) > self.extendtick:
406 min, max = self.extendminmax(min, max, self.ticklist[self.extendtick], extendmin, extendmax)
407 if self.extendlabel is not None and len(self.labellist) > self.extendlabel:
408 min, max = self.extendminmax(min, max, self.labellist[self.extendlabel], extendmin, extendmax)
410 ticks = []
411 for i in range(len(self.ticklist)):
412 ticks = _mergeticklists(ticks, self.getticks(min, max, self.ticklist[i], ticklevel = i))
413 for i in range(len(self.labellist)):
414 ticks = _mergeticklists(ticks, self.getticks(min, max, self.labellist[i], labellevel = i))
416 _mergelabels(ticks, self.labels)
418 return ticks
420 def lesspart(self):
421 return None
423 def morepart(self):
424 return None
427 class autolinpart:
428 """automatic linear partition scheme
429 - possible tick distances are explicitly provided to the constructor
430 - tick distances are adjusted to the axis range by multiplication or division by 10"""
432 __implements__ = _Ipart
434 defaultvariants = ((frac((1, 1)), frac((1, 2))),
435 (frac((2, 1)), frac((1, 1))),
436 (frac((5, 2)), frac((5, 4))),
437 (frac((5, 1)), frac((5, 2))))
439 def __init__(self, variants=defaultvariants, extendtick=0, epsilon=1e-10):
440 """configuration of the partition scheme
441 - variants is a list of tickdist
442 - tickdist should be a list, where the first value
443 is the distance between ticks with ticklevel 0,
444 the second for ticklevel 1, etc.
445 - tickdist values are passed to the frac constructor
446 - labellevel is set to None except for those ticks in the partitions,
447 where ticklevel is zero. There labellevel is also set to zero.
448 - extendtick allows for the extension of the range given to the
449 defaultpart method to include the next tick with the specified
450 level (None turns off this feature); note, that this feature is
451 also disabled, when an axis prohibits its range extension by
452 the extendmin/extendmax variables given to the defaultpart method
453 - epsilon allows for exceeding the axis range by this relative
454 value (relative to the axis range given to the defaultpart method)
455 without creating another tick specified by extendtick"""
456 self.variants = variants
457 self.extendtick = extendtick
458 self.epsilon = epsilon
460 def defaultpart(self, min, max, extendmin, extendmax):
461 logmm = math.log(max - min) / math.log(10)
462 if logmm < 0: # correction for rounding towards zero of the int routine
463 base = frac((10L, 1), int(logmm - 1))
464 else:
465 base = frac((10L, 1), int(logmm))
466 ticks = map(frac, self.variants[0])
467 useticks = [tick * base for tick in ticks]
468 self.lesstickindex = self.moretickindex = 0
469 self.lessbase = frac((base.enum, base.denom))
470 self.morebase = frac((base.enum, base.denom))
471 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
472 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
473 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
475 def lesspart(self):
476 if self.lesstickindex < len(self.variants) - 1:
477 self.lesstickindex += 1
478 else:
479 self.lesstickindex = 0
480 self.lessbase.enum *= 10
481 ticks = map(frac, self.variants[self.lesstickindex])
482 useticks = [tick * self.lessbase for tick in ticks]
483 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
484 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
486 def morepart(self):
487 if self.moretickindex:
488 self.moretickindex -= 1
489 else:
490 self.moretickindex = len(self.variants) - 1
491 self.morebase.denom *= 10
492 ticks = map(frac, self.variants[self.moretickindex])
493 useticks = [tick * self.morebase for tick in ticks]
494 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
495 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
498 class preexp:
499 """storage class for the definition of logarithmic axes partitions
500 instances of this class define tick positions suitable for
501 logarithmic axes by the following instance variables:
502 - exp: integer, which defines multiplicator (usually 10)
503 - pres: list of tick positions (rational numbers, e.g. instances of frac)
504 possible positions are these tick positions and arbitrary divisions
505 and multiplications by the exp value"""
507 def __init__(self, pres, exp):
508 "create a preexp instance and store its pres and exp information"
509 self.pres = helper.ensuresequence(pres)
510 self.exp = exp
513 class logpart(linpart):
514 """logarithmic partition scheme
515 ticks and label positions are explicitly provided to the constructor"""
517 __implements__ = _Ipart
519 pre1exp5 = preexp(frac((1, 1)), 100000)
520 pre1exp4 = preexp(frac((1, 1)), 10000)
521 pre1exp3 = preexp(frac((1, 1)), 1000)
522 pre1exp2 = preexp(frac((1, 1)), 100)
523 pre1exp = preexp(frac((1, 1)), 10)
524 pre125exp = preexp((frac((1, 1)), frac((2, 1)), frac((5, 1))), 10)
525 pre1to9exp = preexp(map(lambda x: frac((x, 1)), range(1, 10)), 10)
526 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
528 def __init__(self, tickpos=None, labelpos=None, labels=None, extendtick=0, extendlabel=None, epsilon=1e-10):
529 """configuration of the partition scheme
530 - tickpos and labelpos should be a list, where the first entry
531 is a preexp instance describing ticks with ticklevel/labellevel 0,
532 the second is a preexp instance for ticklevel/labellevel 1, etc.;
533 a single entry is allowed without being a list
534 - when labelpos is None and tickpos is not None, the tick entries
535 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
536 - labels are applied to the resulting partition via the
537 mergetexts function (additional information available there)
538 - extendtick allows for the extension of the range given to the
539 defaultpart method to include the next tick with the specified
540 level (None turns off this feature); note, that this feature is
541 also disabled, when an axis prohibits its range extension by
542 the extendmin/extendmax variables given to the defaultpart method
543 - extendlabel is analogous to extendtick, but for labels
544 - epsilon allows for exceeding the axis range by this relative
545 logarithm value (relative to the logarithm axis range given
546 to the defaultpart method) without creating another tick
547 specified by extendtick/extendlabel"""
548 if tickpos is None and labels is not None:
549 self.ticklist = (helper.ensuresequence(labelpos)[0],)
550 else:
551 self.ticklist = helper.ensuresequence(tickpos)
553 if labelpos is None and tickpos is not None:
554 self.labellist = (helper.ensuresequence(tickpos)[0],)
555 else:
556 self.labellist = helper.ensuresequence(labelpos)
557 self.labels = labels
558 self.extendtick = extendtick
559 self.extendlabel = extendlabel
560 self.epsilon = epsilon
562 def extendminmax(self, min, max, preexp, extendmin, extendmax):
563 """return new min, max tuple extending the range min, max
564 preexp describes the allowed tick positions
565 extendmin and extendmax are booleans to allow for the extension"""
566 minpower = None
567 maxpower = None
568 for i in xrange(len(preexp.pres)):
569 imin = int(math.floor(math.log(min / float(preexp.pres[i])) /
570 math.log(preexp.exp) + self.epsilon)) + 1
571 imax = int(math.ceil(math.log(max / float(preexp.pres[i])) /
572 math.log(preexp.exp) - self.epsilon)) - 1
573 if minpower is None or imin < minpower:
574 minpower, minindex = imin, i
575 if maxpower is None or imax >= maxpower:
576 maxpower, maxindex = imax, i
577 if minindex:
578 minfrac = preexp.pres[minindex - 1]
579 else:
580 minfrac = preexp.pres[-1]
581 minpower -= 1
582 if maxindex != len(preexp.pres) - 1:
583 maxfrac = preexp.pres[maxindex + 1]
584 else:
585 maxfrac = preexp.pres[0]
586 maxpower += 1
587 if extendmin:
588 min = float(minfrac) * float(preexp.exp) ** minpower
589 if extendmax:
590 max = float(maxfrac) * float(preexp.exp) ** maxpower
591 return min, max
593 def getticks(self, min, max, preexp, ticklevel=None, labellevel=None):
594 """return a list of ticks
595 - preexp describes the allowed tick positions
596 - the ticklevel of the ticks is set to ticklevel and
597 the labellevel is set to labellevel
598 - min, max is the range where ticks should be placed"""
599 ticks = []
600 minimin = 0
601 maximax = 0
602 for f in preexp.pres:
603 fracticks = []
604 imin = int(math.ceil(math.log(min / float(f)) /
605 math.log(preexp.exp) - 0.5 * self.epsilon))
606 imax = int(math.floor(math.log(max / float(f)) /
607 math.log(preexp.exp) + 0.5 * self.epsilon))
608 for i in range(imin, imax + 1):
609 pos = f * frac((preexp.exp, 1), i)
610 fracticks.append(tick((pos.enum, pos.denom), ticklevel = ticklevel, labellevel = labellevel))
611 ticks = _mergeticklists(ticks, fracticks)
612 return ticks
615 class autologpart(logpart):
616 """automatic logarithmic partition scheme
617 possible tick positions are explicitly provided to the constructor"""
619 __implements__ = _Ipart
621 defaultvariants = (((logpart.pre1exp, # ticks
622 logpart.pre1to9exp), # subticks
623 (logpart.pre1exp, # labels
624 logpart.pre125exp)), # sublevels
626 ((logpart.pre1exp, # ticks
627 logpart.pre1to9exp), # subticks
628 None), # labels like ticks
630 ((logpart.pre1exp2, # ticks
631 logpart.pre1exp), # subticks
632 None), # labels like ticks
634 ((logpart.pre1exp3, # ticks
635 logpart.pre1exp), # subticks
636 None), # labels like ticks
638 ((logpart.pre1exp4, # ticks
639 logpart.pre1exp), # subticks
640 None), # labels like ticks
642 ((logpart.pre1exp5, # ticks
643 logpart.pre1exp), # subticks
644 None)) # labels like ticks
646 def __init__(self, variants=defaultvariants, extendtick=0, extendlabel=None, epsilon=1e-10):
647 """configuration of the partition scheme
648 - variants should be a list of pairs of lists of preexp
649 instances
650 - within each pair the first list contains preexp, where
651 the first preexp instance describes ticks positions with
652 ticklevel 0, the second preexp for ticklevel 1, etc.
653 - the second list within each pair describes the same as
654 before, but for labels
655 - within each pair: when the second entry (for the labels) is None
656 and the first entry (for the ticks) ticks is not None, the tick
657 entries for ticklevel 0 are used for labels and vice versa
658 (ticks<->labels)
659 - extendtick allows for the extension of the range given to the
660 defaultpart method to include the next tick with the specified
661 level (None turns off this feature); note, that this feature is
662 also disabled, when an axis prohibits its range extension by
663 the extendmin/extendmax variables given to the defaultpart method
664 - extendlabel is analogous to extendtick, but for labels
665 - epsilon allows for exceeding the axis range by this relative
666 logarithm value (relative to the logarithm axis range given
667 to the defaultpart method) without creating another tick
668 specified by extendtick/extendlabel"""
669 self.variants = variants
670 if len(variants) > 2:
671 self.variantsindex = divmod(len(variants), 2)[0]
672 else:
673 self.variantsindex = 0
674 self.extendtick = extendtick
675 self.extendlabel = extendlabel
676 self.epsilon = epsilon
678 def defaultpart(self, min, max, extendmin, extendmax):
679 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
680 self.morevariantsindex = self.variantsindex
681 self.lessvariantsindex = self.variantsindex
682 part = logpart(tickpos=self.variants[self.variantsindex][0], labelpos=self.variants[self.variantsindex][1],
683 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
684 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
686 def lesspart(self):
687 self.lessvariantsindex += 1
688 if self.lessvariantsindex < len(self.variants):
689 part = logpart(tickpos=self.variants[self.lessvariantsindex][0], labelpos=self.variants[self.lessvariantsindex][1],
690 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
691 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
693 def morepart(self):
694 self.morevariantsindex -= 1
695 if self.morevariantsindex >= 0:
696 part = logpart(tickpos=self.variants[self.morevariantsindex][0], labelpos=self.variants[self.morevariantsindex][1],
697 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
698 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
702 ################################################################################
703 # rater
704 # conseptional remarks:
705 # - raters are used to calculate a rating for a realization of something
706 # - here, a rating means a positive floating point value
707 # - ratings are used to order those realizations by their suitability (lower
708 # ratings are better)
709 # - a rating of None means not suitable at all (those realizations should be
710 # thrown out)
711 ################################################################################
714 class cuberater:
715 """a value rater
716 - a cube rater has an optimal value, where the rate becomes zero
717 - for a left (below the optimum) and a right value (above the optimum),
718 the rating is value is set to 1 (modified by an overall weight factor
719 for the rating)
720 - the analytic form of the rating is cubic for both, the left and
721 the right side of the rater, independently"""
723 # __implements__ = sole implementation
725 def __init__(self, opt, left=None, right=None, weight=1):
726 """initializes the rater
727 - by default, left is set to zero, right is set to 3*opt
728 - left should be smaller than opt, right should be bigger than opt
729 - weight should be positive and is a factor multiplicated to the rates"""
730 if left is None:
731 left = 0
732 if right is None:
733 right = 3*opt
734 self.opt = opt
735 self.left = left
736 self.right = right
737 self.weight = weight
739 def rate(self, value, density):
740 """returns a rating for a value
741 - the density lineary rescales the rater (the optimum etc.),
742 e.g. a value bigger than one increases the optimum (when it is
743 positive) and a value lower than one decreases the optimum (when
744 it is positive); the density itself should be positive"""
745 opt = self.opt * density
746 if value < opt:
747 other = self.left * density
748 elif value > opt:
749 other = self.right * density
750 else:
751 return 0
752 factor = (value - opt) / float(other - opt)
753 return self.weight * (factor ** 3)
756 class distancerater:
757 # TODO: update docstring
758 """a distance rater (rates a list of distances)
759 - the distance rater rates a list of distances by rating each independently
760 and returning the average rate
761 - there is an optimal value, where the rate becomes zero
762 - the analytic form is linary for values above the optimal value
763 (twice the optimal value has the rating one, three times the optimal
764 value has the rating two, etc.)
765 - the analytic form is reciprocal subtracting one for values below the
766 optimal value (halve the optimal value has the rating one, one third of
767 the optimal value has the rating two, etc.)"""
769 # __implements__ = sole implementation
771 def __init__(self, opt, weight=0.1):
772 """inititializes the rater
773 - opt is the optimal length (a visual PyX length)
774 - weight should be positive and is a factor multiplicated to the rates"""
775 self.opt_str = opt
776 self.weight = weight
778 def rate(self, distances, density):
779 """rate distances
780 - the distances are a list of positive floats in PostScript points
781 - the density lineary rescales the rater (the optimum etc.),
782 e.g. a value bigger than one increases the optimum (when it is
783 positive) and a value lower than one decreases the optimum (when
784 it is positive); the density itself should be positive"""
785 if len(distances):
786 opt = unit.topt(unit.length(self.opt_str, default_type="v")) / density
787 rate = 0
788 for distance in distances:
789 if distance < opt:
790 rate += self.weight * (opt / distance - 1)
791 else:
792 rate += self.weight * (distance / opt - 1)
793 return rate / float(len(distances))
796 class axisrater:
797 """a rater for ticks
798 - the rating of axes is splited into two separate parts:
799 - rating of the ticks in terms of the number of ticks, subticks,
800 labels, etc.
801 - rating of the label distances
802 - in the end, a rate for ticks is the sum of these rates
803 - it is useful to first just rate the number of ticks etc.
804 and selecting those partitions, where this fits well -> as soon
805 as an complete rate (the sum of both parts from the list above)
806 of a first ticks is below a rate of just the number of ticks,
807 subticks labels etc. of other ticks, those other ticks will never
808 be better than the first one -> we gain speed by minimizing the
809 number of ticks, where label distances have to be taken into account)
810 - both parts of the rating are shifted into instances of raters
811 defined above --- right now, there is not yet a strict interface
812 for this delegation (should be done as soon as it is needed)"""
814 # __implements__ = sole implementation
816 linticks = (cuberater(4), cuberater(10, weight=0.5), )
817 linlabels = (cuberater(4), )
818 logticks = (cuberater(5, right=20), cuberater(20, right=100, weight=0.5), )
819 loglabels = (cuberater(5, right=20), cuberater(5, left=-20, right=20, weight=0.5), )
820 stdtickrange = cuberater(1, weight=2)
821 stddistance = distancerater("1 cm")
823 def __init__(self, ticks=linticks, labels=linlabels, tickrange=stdtickrange, distance=stddistance):
824 """initializes the axis rater
825 - ticks and labels are lists of instances of a value rater
826 - the first entry in ticks rate the number of ticks, the
827 second the number of subticks, etc.; when there are no
828 ticks of a level or there is not rater for a level, the
829 level is just ignored
830 - labels is analogous, but for labels
831 - within the rating, all ticks with a higher level are
832 considered as ticks for a given level
833 - tickrange is a value rater instance, which rates the covering
834 of an axis range by the ticks (as a relative value of the
835 tick range vs. the axis range), ticks might cover less or
836 more than the axis range (for the standard automatic axis
837 partition schemes an extention of the axis range is normal
838 and should get some penalty)
839 - distance is an distance rater instance"""
840 self.rateticks = ticks
841 self.ratelabels = labels
842 self.tickrange = tickrange
843 self.distance = distance
845 def ratepart(self, axis, ticks, density):
846 """rates ticks by the number of ticks, subticks, labels etc.
847 - takes into account the number of ticks, subticks, labels
848 etc. and the coverage of the axis range by the ticks
849 - when there are no ticks of a level or there was not rater
850 given in the constructor for a level, the level is just
851 ignored
852 - the method returns the sum of the rating results divided
853 by the sum of the weights of the raters
854 - within the rating, all ticks with a higher level are
855 considered as ticks for a given level"""
856 maxticklevel = maxlabellevel = 0
857 for tick in ticks:
858 if tick.ticklevel >= maxticklevel:
859 maxticklevel = tick.ticklevel + 1
860 if tick.labellevel >= maxlabellevel:
861 maxlabellevel = tick.labellevel + 1
862 numticks = [0]*maxticklevel
863 numlabels = [0]*maxlabellevel
864 for tick in ticks:
865 if tick.ticklevel is not None:
866 for level in range(tick.ticklevel, maxticklevel):
867 numticks[level] += 1
868 if tick.labellevel is not None:
869 for level in range(tick.labellevel, maxlabellevel):
870 numlabels[level] += 1
871 rate = 0
872 weight = 0
873 for numtick, rater in zip(numticks, self.rateticks):
874 rate += rater.rate(numtick, density)
875 weight += rater.weight
876 for numlabel, rater in zip(numlabels, self.ratelabels):
877 rate += rater.rate(numlabel, density)
878 weight += rater.weight
879 if len(ticks):
880 # XXX: tickrange was not yet applied
881 # TODO: density == 1?
882 if axis.divisor is not None: # XXX workaround for timeaxis
883 rate += self.tickrange.rate(axis.convert(float(ticks[-1]) * axis.divisor) -
884 axis.convert(float(ticks[0]) * axis.divisor), 1)
885 else:
886 rate += self.tickrange.rate(axis.convert(ticks[-1]) -
887 axis.convert(ticks[0]), 1)
888 else:
889 rate += self.tickrange.rate(0, 1)
890 weight += self.tickrange.weight
891 return rate/weight
893 def ratelayout(self, axiscanvas, density):
894 """rate distances of the labels in an axis canvas
895 - the distances should be collected as box distances of
896 subsequent labels
897 - the axiscanvas provides a labels attribute for easy
898 access to the labels whose distances have to be taken
899 into account
900 - the density is used within the distancerate instance"""
901 if len(axiscanvas.labels) > 1:
902 try:
903 distances = [axiscanvas.labels[i]._boxdistance(axiscanvas.labels[i+1]) for i in range(len(axiscanvas.labels) - 1)]
904 except box.BoxCrossError:
905 return None
906 return self.distance.rate(distances, density)
907 else:
908 return None
911 ################################################################################
912 # texter
913 # texter automatically create labels for tick instances
914 ################################################################################
917 class _Itexter:
919 def labels(self, ticks):
920 """fill the label attribute of ticks
921 - ticks is a list of instances of tick
922 - for each element of ticks the value of the attribute label is set to
923 a string appropriate to the attributes enum and denom of that tick
924 instance
925 - label attributes of the tick instances are just kept, whenever they
926 are not equal to None
927 - the method might extend the labelattrs attribute of the ticks"""
930 class rationaltexter:
931 "a texter creating rational labels (e.g. 'a/b' or even 'a \over b')"
932 # XXX: we use divmod here to be more expicit
934 __implements__ = _Itexter
936 def __init__(self, prefix="", infix="", suffix="",
937 enumprefix="", enuminfix="", enumsuffix="",
938 denomprefix="", denominfix="", denomsuffix="",
939 plus="", minus="-", minuspos=0, over=r"{{%s}\over{%s}}",
940 equaldenom=0, skip1=1, skipenum0=1, skipenum1=1, skipdenom1=1,
941 labelattrs=textmodule.mathmode):
942 r"""initializes the instance
943 - prefix, infix, and suffix (strings) are added at the begin,
944 immediately after the minus, and at the end of the label,
945 respectively
946 - prefixenum, infixenum, and suffixenum (strings) are added
947 to the labels enumerator correspondingly
948 - prefixdenom, infixdenom, and suffixdenom (strings) are added
949 to the labels denominator correspondingly
950 - plus or minus (string) is inserted for non-negative or negative numbers
951 - minuspos is an integer, which determines the position, where the
952 plus or minus sign has to be placed; the following values are allowed:
953 1 - writes the plus or minus in front of the enumerator
954 0 - writes the plus or minus in front of the hole fraction
955 -1 - writes the plus or minus in front of the denominator
956 - over (string) is taken as a format string generating the
957 fraction bar; it has to contain exactly two string insert
958 operators "%s" -- the first for the enumerator and the second
959 for the denominator; by far the most common examples are
960 r"{{%s}\over{%s}}" and "{{%s}/{%s}}"
961 - usually the enumerator and denominator are canceled; however,
962 when equaldenom is set, the least common multiple of all
963 denominators is used
964 - skip1 (boolean) just prints the prefix, the plus or minus,
965 the infix and the suffix, when the value is plus or minus one
966 and at least one of prefix, infix and the suffix is present
967 - skipenum0 (boolean) just prints a zero instead of
968 the hole fraction, when the enumerator is zero;
969 no prefixes, infixes, and suffixes are taken into account
970 - skipenum1 (boolean) just prints the enumprefix, the plus or minus,
971 the enuminfix and the enumsuffix, when the enum value is plus or minus one
972 and at least one of enumprefix, enuminfix and the enumsuffix is present
973 - skipdenom1 (boolean) just prints the enumerator instead of
974 the hole fraction, when the denominator is one and none of the parameters
975 denomprefix, denominfix and denomsuffix are set and minuspos is not -1 or the
976 fraction is positive
977 - labelattrs is a list of attributes for a texrunners text method;
978 a single is allowed without being a list; None is considered as
979 an empty list"""
980 self.prefix = prefix
981 self.infix = infix
982 self.suffix = suffix
983 self.enumprefix = enumprefix
984 self.enuminfix = enuminfix
985 self.enumsuffix = enumsuffix
986 self.denomprefix = denomprefix
987 self.denominfix = denominfix
988 self.denomsuffix = denomsuffix
989 self.plus = plus
990 self.minus = minus
991 self.minuspos = minuspos
992 self.over = over
993 self.equaldenom = equaldenom
994 self.skip1 = skip1
995 self.skipenum0 = skipenum0
996 self.skipenum1 = skipenum1
997 self.skipdenom1 = skipdenom1
998 self.labelattrs = helper.ensurelist(labelattrs)
1000 def gcd(self, *n):
1001 """returns the greates common divisor of all elements in n
1002 - the elements of n must be non-negative integers
1003 - return None if the number of elements is zero
1004 - the greates common divisor is not affected when some
1005 of the elements are zero, but it becomes zero when
1006 all elements are zero"""
1007 if len(n) == 2:
1008 i, j = n
1009 if i < j:
1010 i, j = j, i
1011 while j > 0:
1012 i, (dummy, j) = j, divmod(i, j)
1013 return i
1014 if len(n):
1015 res = n[0]
1016 for i in n[1:]:
1017 res = self.gcd(res, i)
1018 return res
1020 def lcm(self, *n):
1021 """returns the least common multiple of all elements in n
1022 - the elements of n must be non-negative integers
1023 - return None if the number of elements is zero
1024 - the least common multiple is zero when some of the
1025 elements are zero"""
1026 if len(n):
1027 res = n[0]
1028 for i in n[1:]:
1029 res = divmod(res * i, self.gcd(res, i))[0]
1030 return res
1032 def labels(self, ticks):
1033 labeledticks = []
1034 for tick in ticks:
1035 if tick.label is None and tick.labellevel is not None:
1036 labeledticks.append(tick)
1037 tick.temp_fracenum = tick.enum
1038 tick.temp_fracdenom = tick.denom
1039 tick.temp_fracminus = 1
1040 if tick.temp_fracenum < 0:
1041 tick.temp_fracminus = -tick.temp_fracminus
1042 tick.temp_fracenum = -tick.temp_fracenum
1043 if tick.temp_fracdenom < 0:
1044 tick.temp_fracminus = -tick.temp_fracminus
1045 tick.temp_fracdenom = -tick.temp_fracdenom
1046 gcd = self.gcd(tick.temp_fracenum, tick.temp_fracdenom)
1047 (tick.temp_fracenum, dummy1), (tick.temp_fracdenom, dummy2) = divmod(tick.temp_fracenum, gcd), divmod(tick.temp_fracdenom, gcd)
1048 if self.equaldenom:
1049 equaldenom = self.lcm(*[tick.temp_fracdenom for tick in ticks if tick.label is None])
1050 if equaldenom is not None:
1051 for tick in labeledticks:
1052 factor, dummy = divmod(equaldenom, tick.temp_fracdenom)
1053 tick.temp_fracenum, tick.temp_fracdenom = factor * tick.temp_fracenum, factor * tick.temp_fracdenom
1054 for tick in labeledticks:
1055 fracminus = fracenumminus = fracdenomminus = ""
1056 if tick.temp_fracminus == -1:
1057 plusminus = self.minus
1058 else:
1059 plusminus = self.plus
1060 if self.minuspos == 0:
1061 fracminus = plusminus
1062 elif self.minuspos == 1:
1063 fracenumminus = plusminus
1064 elif self.minuspos == -1:
1065 fracdenomminus = plusminus
1066 else:
1067 raise RuntimeError("invalid minuspos")
1068 if self.skipenum0 and tick.temp_fracenum == 0:
1069 tick.label = "0"
1070 elif (self.skip1 and self.skipdenom1 and tick.temp_fracenum == 1 and tick.temp_fracdenom == 1 and
1071 (len(self.prefix) or len(self.infix) or len(self.suffix)) and
1072 not len(fracenumminus) and not len(self.enumprefix) and not len(self.enuminfix) and not len(self.enumsuffix) and
1073 not len(fracdenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix)):
1074 tick.label = "%s%s%s%s" % (self.prefix, fracminus, self.infix, self.suffix)
1075 else:
1076 if self.skipenum1 and tick.temp_fracenum == 1 and (len(self.enumprefix) or len(self.enuminfix) or len(self.enumsuffix)):
1077 tick.temp_fracenum = "%s%s%s%s" % (self.enumprefix, fracenumminus, self.enuminfix, self.enumsuffix)
1078 else:
1079 tick.temp_fracenum = "%s%s%s%i%s" % (self.enumprefix, fracenumminus, self.enuminfix, tick.temp_fracenum, self.enumsuffix)
1080 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):
1081 frac = tick.temp_fracenum
1082 else:
1083 tick.temp_fracdenom = "%s%s%s%i%s" % (self.denomprefix, fracdenomminus, self.denominfix, tick.temp_fracdenom, self.denomsuffix)
1084 frac = self.over % (tick.temp_fracenum, tick.temp_fracdenom)
1085 tick.label = "%s%s%s%s%s" % (self.prefix, fracminus, self.infix, frac, self.suffix)
1086 tick.labelattrs.extend(self.labelattrs)
1088 # del tick.temp_fracenum # we've inserted those temporary variables ... and do not care any longer about them
1089 # del tick.temp_fracdenom
1090 # del tick.temp_fracminus
1094 class decimaltexter:
1095 "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')"
1097 __implements__ = _Itexter
1099 def __init__(self, prefix="", infix="", suffix="", equalprecision=0,
1100 decimalsep=".", thousandsep="", thousandthpartsep="",
1101 plus="", minus="-", period=r"\overline{%s}", labelattrs=textmodule.mathmode):
1102 r"""initializes the instance
1103 - prefix, infix, and suffix (strings) are added at the begin,
1104 immediately after the minus, and at the end of the label,
1105 respectively
1106 - decimalsep, thousandsep, and thousandthpartsep (strings)
1107 are used as separators
1108 - plus or minus (string) is inserted for non-negative or negative numbers
1109 - period (string) is taken as a format string generating a period;
1110 it has to contain exactly one string insert operators "%s" for the
1111 period; usually it should be r"\overline{%s}"
1112 - labelattrs is a list of attributes for a texrunners text method;
1113 a single is allowed without being a list; None is considered as
1114 an empty list"""
1115 self.prefix = prefix
1116 self.infix = infix
1117 self.suffix = suffix
1118 self.equalprecision = equalprecision
1119 self.decimalsep = decimalsep
1120 self.thousandsep = thousandsep
1121 self.thousandthpartsep = thousandthpartsep
1122 self.plus = plus
1123 self.minus = minus
1124 self.period = period
1125 self.labelattrs = helper.ensurelist(labelattrs)
1127 def labels(self, ticks):
1128 labeledticks = []
1129 maxdecprecision = 0
1130 for tick in ticks:
1131 if tick.label is None and tick.labellevel is not None:
1132 labeledticks.append(tick)
1133 m, n = tick.enum, tick.denom
1134 if m < 0: m = -m
1135 if n < 0: n = -n
1136 whole, reminder = divmod(m, n)
1137 whole = str(whole)
1138 if len(self.thousandsep):
1139 l = len(whole)
1140 tick.label = ""
1141 for i in range(l):
1142 tick.label += whole[i]
1143 if not ((l-i-1) % 3) and l > i+1:
1144 tick.label += self.thousandsep
1145 else:
1146 tick.label = whole
1147 if reminder:
1148 tick.label += self.decimalsep
1149 oldreminders = []
1150 tick.temp_decprecision = 0
1151 while (reminder):
1152 tick.temp_decprecision += 1
1153 if reminder in oldreminders:
1154 tick.temp_decprecision = None
1155 periodstart = len(tick.label) - (len(oldreminders) - oldreminders.index(reminder))
1156 tick.label = tick.label[:periodstart] + self.period % tick.label[periodstart:]
1157 break
1158 oldreminders += [reminder]
1159 reminder *= 10
1160 whole, reminder = divmod(reminder, n)
1161 if not ((tick.temp_decprecision - 1) % 3) and tick.temp_decprecision > 1:
1162 tick.label += self.thousandthpartsep
1163 tick.label += str(whole)
1164 if maxdecprecision < tick.temp_decprecision:
1165 maxdecprecision = tick.temp_decprecision
1166 if self.equalprecision:
1167 for tick in labeledticks:
1168 if tick.temp_decprecision is not None:
1169 if tick.temp_decprecision == 0 and maxdecprecision > 0:
1170 tick.label += self.decimalsep
1171 for i in range(tick.temp_decprecision, maxdecprecision):
1172 if not ((i - 1) % 3) and i > 1:
1173 tick.label += self.thousandthpartsep
1174 tick.label += "0"
1175 for tick in labeledticks:
1176 if tick.enum * tick.denom < 0:
1177 plusminus = self.minus
1178 else:
1179 plusminus = self.plus
1180 tick.label = "%s%s%s%s%s" % (self.prefix, plusminus, self.infix, tick.label, self.suffix)
1181 tick.labelattrs.extend(self.labelattrs)
1183 # del tick.temp_decprecision # we've inserted this temporary variable ... and do not care any longer about it
1186 class exponentialtexter:
1187 "a texter creating labels with exponentials (e.g. '2\cdot10^5')"
1189 __implements__ = _Itexter
1191 def __init__(self, plus="", minus="-",
1192 mantissaexp=r"{{%s}\cdot10^{%s}}",
1193 nomantissaexp=r"{10^{%s}}",
1194 minusnomantissaexp=r"{-10^{%s}}",
1195 mantissamin=frac((1, 1)), mantissamax=frac((10, 1)),
1196 skipmantissa1=0, skipallmantissa1=1,
1197 mantissatexter=decimaltexter()):
1198 r"""initializes the instance
1199 - plus or minus (string) is inserted for non-negative or negative exponents
1200 - mantissaexp (string) is taken as a format string generating the exponent;
1201 it has to contain exactly two string insert operators "%s" --
1202 the first for the mantissa and the second for the exponent;
1203 examples are r"{{%s}\cdot10^{%s}}" and r"{{%s}{\rm e}^{%s}}"
1204 - nomantissaexp (string) is taken as a format string generating the exponent
1205 when the mantissa is one and should be skipped; it has to contain
1206 exactly one string insert operators "%s" for the exponent;
1207 an examples is r"{10^{%s}}"
1208 - minusnomantissaexp (string) is taken as a format string generating the exponent
1209 when the mantissa is minus one and should be skipped; it has to contain
1210 exactly one string insert operators "%s" for the exponent; might be set to None
1211 to disallow skipping of any mantissa minus one
1212 an examples is r"{-10^{%s}}"
1213 - mantissamin and mantissamax are the minimum and maximum of the mantissa;
1214 they are frac instances greater than zero and mantissamin < mantissamax;
1215 the sign of the tick is ignored here
1216 - skipmantissa1 (boolean) turns on skipping of any mantissa equals one
1217 (and minus when minusnomantissaexp is set)
1218 - skipallmantissa1 (boolean) as above, but all mantissas must be 1
1219 - mantissatexter is the texter for the mantissa"""
1220 self.plus = plus
1221 self.minus = minus
1222 self.mantissaexp = mantissaexp
1223 self.nomantissaexp = nomantissaexp
1224 self.minusnomantissaexp = minusnomantissaexp
1225 self.mantissamin = mantissamin
1226 self.mantissamax = mantissamax
1227 self.mantissamindivmax = self.mantissamin / self.mantissamax
1228 self.mantissamaxdivmin = self.mantissamax / self.mantissamin
1229 self.skipmantissa1 = skipmantissa1
1230 self.skipallmantissa1 = skipallmantissa1
1231 self.mantissatexter = mantissatexter
1233 def labels(self, ticks):
1234 labeledticks = []
1235 for tick in ticks:
1236 if tick.label is None and tick.labellevel is not None:
1237 tick.temp_orgenum, tick.temp_orgdenom = tick.enum, tick.denom
1238 labeledticks.append(tick)
1239 tick.temp_exp = 0
1240 if tick.enum:
1241 while abs(tick) >= self.mantissamax:
1242 tick.temp_exp += 1
1243 x = tick * self.mantissamindivmax
1244 tick.enum, tick.denom = x.enum, x.denom
1245 while abs(tick) < self.mantissamin:
1246 tick.temp_exp -= 1
1247 x = tick * self.mantissamaxdivmin
1248 tick.enum, tick.denom = x.enum, x.denom
1249 if tick.temp_exp < 0:
1250 tick.temp_exp = "%s%i" % (self.minus, -tick.temp_exp)
1251 else:
1252 tick.temp_exp = "%s%i" % (self.plus, tick.temp_exp)
1253 self.mantissatexter.labels(labeledticks)
1254 if self.minusnomantissaexp is not None:
1255 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if abs(tick.enum) == abs(tick.denom)])
1256 else:
1257 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if tick.enum == tick.denom])
1258 for tick in labeledticks:
1259 if (self.skipallmantissa1 and allmantissa1 or
1260 (self.skipmantissa1 and (tick.enum == tick.denom or
1261 (tick.enum == -tick.denom and self.minusnomantissaexp is not None)))):
1262 if tick.enum == tick.denom:
1263 tick.label = self.nomantissaexp % tick.temp_exp
1264 else:
1265 tick.label = self.minusnomantissaexp % tick.temp_exp
1266 else:
1267 tick.label = self.mantissaexp % (tick.label, tick.temp_exp)
1268 tick.enum, tick.denom = tick.temp_orgenum, tick.temp_orgdenom
1270 # del tick.temp_orgenum # we've inserted those temporary variables ... and do not care any longer about them
1271 # del tick.temp_orgdenom
1272 # del tick.temp_exp
1275 class defaulttexter:
1276 "a texter creating decimal or exponential labels"
1278 __implements__ = _Itexter
1280 def __init__(self, smallestdecimal=frac((1, 1000)),
1281 biggestdecimal=frac((9999, 1)),
1282 equaldecision=1,
1283 decimaltexter=decimaltexter(),
1284 exponentialtexter=exponentialtexter()):
1285 r"""initializes the instance
1286 - smallestdecimal and biggestdecimal are the smallest and
1287 biggest decimal values, where the decimaltexter should be used;
1288 they are frac instances; the sign of the tick is ignored here;
1289 a tick at zero is considered for the decimaltexter as well
1290 - equaldecision (boolean) uses decimaltexter or exponentialtexter
1291 globaly (set) or for each tick separately (unset)
1292 - decimaltexter and exponentialtexter are texters to be used"""
1293 self.smallestdecimal = smallestdecimal
1294 self.biggestdecimal = biggestdecimal
1295 self.equaldecision = equaldecision
1296 self.decimaltexter = decimaltexter
1297 self.exponentialtexter = exponentialtexter
1299 def labels(self, ticks):
1300 decticks = []
1301 expticks = []
1302 for tick in ticks:
1303 if tick.label is None and tick.labellevel is not None:
1304 if not tick.enum or (abs(tick) >= self.smallestdecimal and abs(tick) <= self.biggestdecimal):
1305 decticks.append(tick)
1306 else:
1307 expticks.append(tick)
1308 if self.equaldecision:
1309 if len(expticks):
1310 self.exponentialtexter.labels(ticks)
1311 else:
1312 self.decimaltexter.labels(ticks)
1313 else:
1314 for tick in decticks:
1315 self.decimaltexter.labels([tick])
1316 for tick in expticks:
1317 self.exponentialtexter.labels([tick])
1320 ################################################################################
1321 # axis painter
1322 ################################################################################
1325 class axiscanvas(canvas._canvas):
1326 """axis canvas
1327 - an axis canvas is a regular canvas to be filled by
1328 a axispainters painter method
1329 - it contains a PyX length extent to be used for the
1330 alignment of additional axes; the axis extent should
1331 be filled by the axispainters painter method; you may
1332 grasp this as a size information comparable to a bounding
1334 - it contains a list of textboxes called labels which are
1335 used to rate the distances between the labels if needed
1336 by the axis later on; the painter method has not only to
1337 insert the labels into this canvas, but should also fill
1338 this list, when a rating of the distances should be
1339 performed by the axis"""
1341 # __implements__ = sole implementation
1343 def __init__(self, texrunner, *args, **kwargs):
1344 """initializes the instance
1345 - sets the texrunner
1346 - sets extent to zero
1347 - sets labels to an empty list"""
1348 canvas._canvas.__init__(self, *args, **kwargs)
1349 self.settexrunner(texrunner)
1350 self.extent = 0
1351 self.labels = []
1354 class rotatetext:
1355 """create rotations accordingly to tick directions
1356 - upsidedown rotations are suppressed by rotating them by another 180 degree"""
1358 # __implements__ = sole implementation
1360 def __init__(self, direction, epsilon=1e-10):
1361 """initializes the instance
1362 - direction is an angle to be used relative to the tick direction
1363 - epsilon is the value by which 90 degrees can be exceeded before
1364 an 180 degree rotation is added"""
1365 self.direction = direction
1366 self.epsilon = epsilon
1368 def trafo(self, dx, dy):
1369 """returns a rotation transformation accordingly to the tick direction
1370 - dx and dy are the direction of the tick"""
1371 direction = self.direction + math.atan2(dy, dx) * 180 / math.pi
1372 while (direction > 90 + self.epsilon):
1373 direction -= 180
1374 while (direction < -90 - self.epsilon):
1375 direction += 180
1376 return trafomodule.rotate(direction)
1379 rotatetext.parallel = rotatetext(-90)
1380 rotatetext.orthogonal = rotatetext(0)
1383 class _Iaxispainter:
1384 "class for painting axes"
1386 def paint(self, axispos, axis, axiscanvas):
1387 """paint the axis into the axiscanvas
1388 - axispos is an instance, which implements _Iaxispos to
1389 define the tick positions
1390 - the axis and should not be modified (we may
1391 add some temporary variables like axis.ticks[i].temp_xxx,
1392 which might be used just temporary) -- the idea is that
1393 all things can be used several times
1394 - also do not modify the instance (self) -- even this
1395 instance might be used several times; thus do not modify
1396 attributes like self.titleattrs etc. (just use a copy of it)
1397 - the method might access some additional attributes from
1398 the axis, e.g. the axis title -- the axis painter should
1399 document this behavior and rely on the availability of
1400 those attributes -> it becomes a question of the proper
1401 usage of the combination of axis & axispainter
1402 - the axiscanvas is a axiscanvas instance and should be
1403 filled with ticks, labels, title, etc.; note that the
1404 extent and labels instance variables should be filled as
1405 documented in the axiscanvas"""
1408 class axistitlepainter:
1409 """class for painting an axis title
1410 - the axis must have a title attribute when using this painter;
1411 this title might be None"""
1413 __implements__ = _Iaxispainter
1415 def __init__(self, titledist="0.3 cm",
1416 titleattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1417 titledirection=rotatetext.parallel,
1418 titlepos=0.5):
1419 """initialized the instance
1420 - titledist is a visual PyX length giving the distance
1421 of the title from the axis extent already there (a title might
1422 be added after labels or other things are plotted already)
1423 - labelattrs is a list of attributes for a texrunners text
1424 method; a single is allowed without being a list; None
1425 turns off the title
1426 - titledirection is an instance of rotatetext or None
1427 - titlepos is the position of the title in graph coordinates"""
1428 self.titledist_str = titledist
1429 self.titleattrs = titleattrs
1430 self.titledirection = titledirection
1431 self.titlepos = titlepos
1433 def paint(self, axispos, axis, axiscanvas):
1434 if axis.title is not None and self.titleattrs is not None:
1435 titledist = unit.length(self.titledist_str, default_type="v")
1436 x, y = axispos._vtickpoint(self.titlepos, axis=axis)
1437 dx, dy = axispos.vtickdirection(self.titlepos, axis=axis)
1438 titleattrs = helper.ensurelist(self.titleattrs)
1439 if self.titledirection is not None:
1440 titleattrs.append(self.titledirection.trafo(dx, dy))
1441 title = axiscanvas.texrunner._text(x, y, axis.title, *titleattrs)
1442 axiscanvas.extent += titledist
1443 title.linealign(axiscanvas.extent, dx, dy)
1444 axiscanvas.extent += title.extent(dx, dy)
1445 axiscanvas.insert(title)
1448 class axispainter(axistitlepainter):
1449 """class for painting the ticks and labels of an axis
1450 - the inherited titleaxispainter is used to paint the title of
1451 the axis
1452 - note that the type of the elements of ticks given as an argument
1453 of the paint method must be suitable for the tick position methods
1454 of the axis"""
1456 __implements__ = _Iaxispainter
1458 defaultticklengths = ["%0.5f cm" % (0.2*goldenmean**(-i)) for i in range(10)]
1460 def __init__(self, innerticklengths=defaultticklengths,
1461 outerticklengths=None,
1462 tickattrs=(),
1463 gridattrs=None,
1464 zerolineattrs=(),
1465 baselineattrs=canvas.linecap.square,
1466 labeldist="0.3 cm",
1467 labelattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1468 labeldirection=None,
1469 labelhequalize=0,
1470 labelvequalize=1,
1471 **kwargs):
1472 """initializes the instance
1473 - innerticklenths and outerticklengths are two lists of
1474 visual PyX lengths for ticks, subticks, etc. plotted inside
1475 and outside of the graph; when a single value is given, it
1476 is used for all tick levels; None turns off ticks inside or
1477 outside of the graph
1478 - tickattrs are a list of stroke attributes for the ticks;
1479 a single entry is allowed without being a list; None turns
1480 off ticks
1481 - gridlineattrs are a list of lists used as stroke
1482 attributes for ticks, subticks etc.; when a single list
1483 is given, it is used for ticks, subticks, etc.; a single
1484 entry is allowed without being a list; None turns off
1485 gridlines
1486 - zerolineattrs are a list of stroke attributes for a grid
1487 line at axis value zero; a single entry is allowed without
1488 being a list; None turns off the zeroline
1489 - baselineattrs are a list of stroke attributes for a grid
1490 line at axis value zero; a single entry is allowed without
1491 being a list; None turns off the baseline
1492 - labeldist is a visual PyX length for the distance of the labels
1493 from the axis baseline
1494 - labelattrs is a list of attributes for a texrunners text
1495 method; a single entry is allowed without being a list;
1496 None turns off the labels
1497 - titledirection is an instance of rotatetext or None
1498 - labelhequalize and labelvequalize (booleans) perform an equal
1499 alignment for straight vertical and horizontal axes, respectively
1500 - futher keyword arguments are passed to axistitlepainter"""
1501 # TODO: access to axis.divisor -- document, remove, ... ???
1502 self.innerticklengths_str = innerticklengths
1503 self.outerticklengths_str = outerticklengths
1504 self.tickattrs = tickattrs
1505 self.gridattrs = gridattrs
1506 self.zerolineattrs = zerolineattrs
1507 self.baselineattrs = baselineattrs
1508 self.labeldist_str = labeldist
1509 self.labelattrs = labelattrs
1510 self.labeldirection = labeldirection
1511 self.labelhequalize = labelhequalize
1512 self.labelvequalize = labelvequalize
1513 axistitlepainter.__init__(self, **kwargs)
1515 def paint(self, axispos, axis, axiscanvas):
1516 labeldist = unit.length(self.labeldist_str, default_type="v")
1517 for tick in axis.ticks:
1518 if axis.divisor is not None: # XXX workaround for timeaxis
1519 tick.temp_v = axis.convert(float(tick) * axis.divisor)
1520 else:
1521 tick.temp_v = axis.convert(tick)
1522 tick.temp_x, tick.temp_y = axispos._vtickpoint(tick.temp_v, axis=axis)
1523 tick.temp_dx, tick.temp_dy = axispos.vtickdirection(tick.temp_v, axis=axis)
1525 # create & align tick.temp_labelbox
1526 for tick in axis.ticks:
1527 if tick.labellevel is not None:
1528 labelattrs = helper.getsequenceno(self.labelattrs, tick.labellevel)
1529 if labelattrs is not None:
1530 labelattrs = helper.ensurelist(labelattrs)[:]
1531 if self.labeldirection is not None:
1532 labelattrs.append(self.labeldirection.trafo(tick.temp_dx, tick.temp_dy))
1533 if tick.labelattrs is not None:
1534 labelattrs.extend(helper.ensurelist(tick.labelattrs))
1535 tick.temp_labelbox = axiscanvas.texrunner._text(tick.temp_x, tick.temp_y, tick.label, *labelattrs)
1536 if len(axis.ticks) > 1:
1537 equaldirection = 1
1538 for tick in axis.ticks[1:]:
1539 if tick.temp_dx != axis.ticks[0].temp_dx or tick.temp_dy != axis.ticks[0].temp_dy:
1540 equaldirection = 0
1541 else:
1542 equaldirection = 0
1543 if equaldirection and ((not axis.ticks[0].temp_dx and self.labelvequalize) or
1544 (not axis.ticks[0].temp_dy and self.labelhequalize)):
1545 if self.labelattrs is not None:
1546 box.linealignequal([tick.temp_labelbox for tick in axis.ticks if tick.labellevel is not None],
1547 labeldist, axis.ticks[0].temp_dx, axis.ticks[0].temp_dy)
1548 else:
1549 for tick in axis.ticks:
1550 if tick.labellevel is not None and self.labelattrs is not None:
1551 tick.temp_labelbox.linealign(labeldist, tick.temp_dx, tick.temp_dy)
1553 def mkv(arg):
1554 if helper.issequence(arg):
1555 return [unit.length(a, default_type="v") for a in arg]
1556 if arg is not None:
1557 return unit.length(arg, default_type="v")
1558 innerticklengths = mkv(self.innerticklengths_str)
1559 outerticklengths = mkv(self.outerticklengths_str)
1561 for tick in axis.ticks:
1562 if tick.ticklevel is not None:
1563 innerticklength = helper.getitemno(innerticklengths, tick.ticklevel)
1564 outerticklength = helper.getitemno(outerticklengths, tick.ticklevel)
1565 if innerticklength is not None or outerticklength is not None:
1566 if innerticklength is None:
1567 innerticklength = 0
1568 if outerticklength is None:
1569 outerticklength = 0
1570 tickattrs = helper.getsequenceno(self.tickattrs, tick.ticklevel)
1571 if tickattrs is not None:
1572 _innerticklength = unit.topt(innerticklength)
1573 _outerticklength = unit.topt(outerticklength)
1574 x1 = tick.temp_x - tick.temp_dx * _innerticklength
1575 y1 = tick.temp_y - tick.temp_dy * _innerticklength
1576 x2 = tick.temp_x + tick.temp_dx * _outerticklength
1577 y2 = tick.temp_y + tick.temp_dy * _outerticklength
1578 axiscanvas.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(tickattrs))
1579 if tick != frac((0, 1)) or self.zerolineattrs is None:
1580 gridattrs = helper.getsequenceno(self.gridattrs, tick.ticklevel)
1581 if gridattrs is not None:
1582 axiscanvas.stroke(axispos.vgridline(tick.temp_v, axis=axis), *helper.ensuresequence(gridattrs))
1583 if outerticklength is not None and unit.topt(outerticklength) > unit.topt(axiscanvas.extent):
1584 axiscanvas.extent = outerticklength
1585 if outerticklength is not None and unit.topt(-innerticklength) > unit.topt(axiscanvas.extent):
1586 axiscanvas.extent = -innerticklength
1587 if tick.labellevel is not None and self.labelattrs is not None:
1588 axiscanvas.insert(tick.temp_labelbox)
1589 axiscanvas.labels.append(tick.temp_labelbox)
1590 extent = tick.temp_labelbox.extent(tick.temp_dx, tick.temp_dy) + labeldist
1591 if unit.topt(extent) > unit.topt(axiscanvas.extent):
1592 axiscanvas.extent = extent
1593 if self.baselineattrs is not None:
1594 axiscanvas.stroke(axispos.vbaseline(axis=axis), *helper.ensuresequence(self.baselineattrs))
1595 if self.zerolineattrs is not None:
1596 if len(axis.ticks) and axis.ticks[0] * axis.ticks[-1] < frac((0, 1)):
1597 axiscanvas.stroke(axispos.gridline(0, axis=axis), *helper.ensuresequence(self.zerolineattrs))
1599 # for tick in axis.ticks:
1600 # del tick.temp_v # we've inserted those temporary variables ... and do not care any longer about them
1601 # del tick.temp_x
1602 # del tick.temp_y
1603 # del tick.temp_dx
1604 # del tick.temp_dy
1605 # if tick.labellevel is not None and self.labelattrs is not None:
1606 # del tick.temp_labelbox
1608 axistitlepainter.paint(self, axispos, axis, axiscanvas)
1611 class linkaxispainter(axispainter):
1612 """class for painting a linked axis
1613 - the inherited axispainter is used to paint the axis
1614 - modifies some constructor defaults"""
1616 __implements__ = _Iaxispainter
1618 def __init__(self, zerolineattrs=None,
1619 labelattrs=None,
1620 titleattrs=None,
1621 **kwargs):
1622 """initializes the instance
1623 - the zerolineattrs default is set to None thus skipping the zeroline
1624 - the labelattrs default is set to None thus skipping the labels
1625 - the titleattrs default is set to None thus skipping the title
1626 - all keyword arguments are passed to axispainter"""
1627 axispainter.__init__(self, zerolineattrs=zerolineattrs,
1628 labelattrs=labelattrs,
1629 titleattrs=titleattrs,
1630 **kwargs)
1633 class splitaxispainter(axistitlepainter):
1634 """class for painting a splitaxis
1635 - the inherited titleaxispainter is used to paint the title of
1636 the axis
1637 - the splitaxispainter access the subaxes attribute of the axis"""
1639 __implements__ = _Iaxispainter
1641 def __init__(self, breaklinesdist="0.05 cm",
1642 breaklineslength="0.5 cm",
1643 breaklinesangle=-60,
1644 breaklinesattrs=(),
1645 **args):
1646 """initializes the instance
1647 - breaklinesdist is a visual length of the distance between
1648 the two lines of the axis break
1649 - breaklineslength is a visual length of the length of the
1650 two lines of the axis break
1651 - breaklinesangle is the angle of the lines of the axis break
1652 - breaklinesattrs are a list of stroke attributes for the
1653 axis break lines; a single entry is allowed without being a
1654 list; None turns off the break lines
1655 - futher keyword arguments are passed to axistitlepainter"""
1656 self.breaklinesdist_str = breaklinesdist
1657 self.breaklineslength_str = breaklineslength
1658 self.breaklinesangle = breaklinesangle
1659 self.breaklinesattrs = breaklinesattrs
1660 axistitlepainter.__init__(self, **args)
1662 def paint(self, axispos, axis, axiscanvas):
1663 for subaxis in axis.subaxes:
1664 subaxis.finish(axis, axiscanvas.texrunner)
1665 axiscanvas.insert(subaxis.axiscanvas)
1666 if unit.topt(axiscanvas.extent) < unit.topt(subaxis.axiscanvas.extent):
1667 axiscanvas.extent = subaxis.axiscanvas.extent
1668 if self.breaklinesattrs is not None:
1669 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1670 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1671 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1672 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1673 breaklinesextent = (0.5*self.breaklinesdist*math.fabs(self.cos) +
1674 0.5*self.breaklineslength*math.fabs(self.sin))
1675 if unit.topt(axiscanvas.extent) < unit.topt(breaklinesextent):
1676 axiscanvas.extent = breaklinesextent
1677 for subaxis1, subaxis2 in zip(axis.subaxes[:-1], axis.subaxes[1:]):
1678 # use a tangent of the baseline (this is independent of the tickdirection)
1679 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1680 breakline = path.normpath(axispos.vbaseline(v, None, axis=axis)).tangent(0, self.breaklineslength)
1681 widthline = path.normpath(axispos.vbaseline(v, None, axis=axis)).tangent(0, self.breaklinesdist).transformed(trafomodule.rotate(self.breaklinesangle+90, *breakline.begin()))
1682 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1683 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1684 breakline = breakline.transformed(trafomodule.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1685 breakline1 = breakline.transformed(trafomodule.translate(*towidth))
1686 breakline2 = breakline.transformed(trafomodule.translate(-towidth[0], -towidth[1]))
1687 axiscanvas.fill(path.path(path.moveto(*breakline1.begin()),
1688 path.lineto(*breakline1.end()),
1689 path.lineto(*breakline2.end()),
1690 path.lineto(*breakline2.begin()),
1691 path.closepath()), color.gray.white)
1692 axiscanvas.stroke(breakline1, *helper.ensuresequence(self.breaklinesattrs))
1693 axiscanvas.stroke(breakline2, *helper.ensuresequence(self.breaklinesattrs))
1694 axistitlepainter.paint(self, axispos, axis, axiscanvas)
1697 class linksplitaxispainter(splitaxispainter):
1698 """class for painting a linked splitaxis
1699 - the inherited splitaxispainter is used to paint the axis
1700 - modifies some constructor defaults"""
1702 __implements__ = _Iaxispainter
1704 def __init__(self, titleattrs=None, **kwargs):
1705 """initializes the instance
1706 - the titleattrs default is set to None thus skipping the title
1707 - all keyword arguments are passed to splitaxispainter"""
1708 splitaxispainter.__init__(self, titleattrs=titleattrs, **kwargs)
1711 class baraxispainter(axistitlepainter):
1712 """class for painting a baraxis
1713 - the inherited titleaxispainter is used to paint the title of
1714 the axis
1715 - the baraxispainter access the multisubaxis, subaxis names, texts, and
1716 relsizes attributes"""
1718 __implements__ = _Iaxispainter
1720 def __init__(self, innerticklength=None,
1721 outerticklength=None,
1722 tickattrs=(),
1723 baselineattrs=canvas.linecap.square,
1724 namedist="0.3 cm",
1725 nameattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1726 namedirection=None,
1727 namepos=0.5,
1728 namehequalize=0,
1729 namevequalize=1,
1730 **args):
1731 """initializes the instance
1732 - innerticklength and outerticklength are a visual length of
1733 the ticks to be plotted at the axis baseline to visually
1734 separate the bars; if neither innerticklength nor
1735 outerticklength are set, not ticks are plotted
1736 - breaklinesattrs are a list of stroke attributes for the
1737 axis tick; a single entry is allowed without being a
1738 list; None turns off the ticks
1739 - namedist is a visual PyX length for the distance of the bar
1740 names from the axis baseline
1741 - nameattrs is a list of attributes for a texrunners text
1742 method; a single entry is allowed without being a list;
1743 None turns off the names
1744 - namedirection is an instance of rotatetext or None
1745 - namehequalize and namevequalize (booleans) perform an equal
1746 alignment for straight vertical and horizontal axes, respectively
1747 - futher keyword arguments are passed to axistitlepainter"""
1748 self.innerticklength_str = innerticklength
1749 self.outerticklength_str = outerticklength
1750 self.tickattrs = tickattrs
1751 self.baselineattrs = baselineattrs
1752 self.namedist_str = namedist
1753 self.nameattrs = nameattrs
1754 self.namedirection = namedirection
1755 self.namepos = namepos
1756 self.namehequalize = namehequalize
1757 self.namevequalize = namevequalize
1758 axistitlepainter.__init__(self, **args)
1760 def paint(self, axispos, axis, axiscanvas):
1761 if axis.multisubaxis is not None:
1762 for subaxis in axis.subaxis:
1763 subaxis.finish(axis, axiscanvas.texrunner)
1764 axiscanvas.insert(subaxis.axiscanvas)
1765 if unit.topt(axiscanvas.extent) < unit.topt(subaxis.axiscanvas.extent):
1766 axiscanvas.extent = subaxis.axiscanvas.extent
1767 namepos = []
1768 for name in axis.names:
1769 v = axis.convert((name, self.namepos))
1770 x, y = axispos._vtickpoint(v, axis=axis)
1771 dx, dy = axispos.vtickdirection(v, axis=axis)
1772 namepos.append((v, x, y, dx, dy))
1773 nameboxes = []
1774 if self.nameattrs is not None:
1775 for (v, x, y, dx, dy), name in zip(namepos, axis.names):
1776 nameattrs = helper.ensurelist(self.nameattrs)[:]
1777 if self.namedirection is not None:
1778 nameattrs.append(self.namedirection.trafo(tick.temp_dx, tick.temp_dy))
1779 if axis.texts.has_key(name):
1780 nameboxes.append(axiscanvas.texrunner._text(x, y, str(axis.texts[name]), *nameattrs))
1781 elif axis.texts.has_key(str(name)):
1782 nameboxes.append(axiscanvas.texrunner._text(x, y, str(axis.texts[str(name)]), *nameattrs))
1783 else:
1784 nameboxes.append(axiscanvas.texrunner._text(x, y, str(name), *nameattrs))
1785 labeldist = axiscanvas.extent + unit.length(self.namedist_str, default_type="v")
1786 if len(namepos) > 1:
1787 equaldirection = 1
1788 for np in namepos[1:]:
1789 if np[3] != namepos[0][3] or np[4] != namepos[0][4]:
1790 equaldirection = 0
1791 else:
1792 equaldirection = 0
1793 if equaldirection and ((not namepos[0][3] and self.namevequalize) or
1794 (not namepos[0][4] and self.namehequalize)):
1795 box.linealignequal(nameboxes, labeldist, namepos[0][3], namepos[0][4])
1796 else:
1797 for namebox, np in zip(nameboxes, namepos):
1798 namebox.linealign(labeldist, np[3], np[4])
1799 if self.innerticklength_str is not None:
1800 innerticklength = unit.length(self.innerticklength_str, default_type="v")
1801 _innerticklength = unit.topt(innerticklength)
1802 if self.tickattrs is not None and unit.topt(axiscanvas.extent) < -_innerticklength:
1803 axiscanvas.extent = -innerticklength
1804 elif self.outerticklength_str is not None:
1805 innerticklength = _innerticklength = 0
1806 if self.outerticklength_str is not None:
1807 outerticklength = unit.length(self.outerticklength_str, default_type="v")
1808 _outerticklength = unit.topt(outerticklength)
1809 if self.tickattrs is not None and unit.topt(axiscanvas.extent) < _outerticklength:
1810 axiscanvas.extent = outerticklength
1811 elif self.innerticklength_str is not None:
1812 outerticklength = _outerticklength = 0
1813 for (v, x, y, dx, dy), namebox in zip(namepos, nameboxes):
1814 newextent = namebox.extent(dx, dy) + labeldist
1815 if unit.topt(axiscanvas.extent) < unit.topt(newextent):
1816 axiscanvas.extent = newextent
1817 if self.tickattrs is not None and (self.innerticklength_str is not None or self.outerticklength_str is not None):
1818 for pos in axis.relsizes:
1819 if pos == axis.relsizes[0]:
1820 pos -= axis.firstdist
1821 elif pos != axis.relsizes[-1]:
1822 pos -= 0.5 * axis.dist
1823 v = pos / axis.relsizes[-1]
1824 x, y = axispos._vtickpoint(v, axis=axis)
1825 dx, dy = axispos.vtickdirection(v, axis=axis)
1826 x1 = x - dx * _innerticklength
1827 y1 = y - dy * _innerticklength
1828 x2 = x + dx * _outerticklength
1829 y2 = y + dy * _outerticklength
1830 axiscanvas.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(self.tickattrs))
1831 if self.baselineattrs is not None:
1832 p = axispos.vbaseline(axis=axis)
1833 if p is not None:
1834 axiscanvas.stroke(axispos.vbaseline(axis=axis), *helper.ensuresequence(self.baselineattrs))
1835 for namebox in nameboxes:
1836 axiscanvas.insert(namebox)
1837 axistitlepainter.paint(self, axispos, axis, axiscanvas)
1840 class linkbaraxispainter(baraxispainter):
1841 """class for painting a linked baraxis
1842 - the inherited baraxispainter is used to paint the axis
1843 - modifies some constructor defaults"""
1845 __implements__ = _Iaxispainter
1847 def __init__(self, nameattrs=None, titleattrs=None, **kwargs):
1848 """initializes the instance
1849 - the titleattrs default is set to None thus skipping the title
1850 - the nameattrs default is set to None thus skipping the names
1851 - all keyword arguments are passed to axispainter"""
1852 baraxispainter.__init__(self, nameattrs=nameattrs, titleattrs=titleattrs, **kwargs)
1855 ################################################################################
1856 # axes
1857 ################################################################################
1860 class _Iaxis:
1861 """interface definition of a axis
1862 - data and tick range are handled separately
1863 - an axis should implement an convert and invert method like
1864 _Imap, but this is not part of this interface definition;
1865 one possibility is to mix-in a proper map class, but special
1866 purpose axes might do something else
1867 - instance variables:
1868 - axiscanvas: an axiscanvas instance, which is available after
1869 calling the finish method (the idea is, that an axis might
1870 fill different axiscanvas instances by calling the painter
1871 for different ticks and rate those ticks afterwards -- thus
1872 the axis finally can set this best canvas as its axis canvas)
1873 - relsize: relative size (width) of the axis (for use
1874 in splitaxis, baraxis etc.) -- this might not be available for
1875 all axes, which will just create an name error right now
1876 - configuration of the range handling is not subject
1877 of this interface definition"""
1878 # TODO: - add a mechanism to allow for a different range
1879 # handling of x and y ranges
1880 # - should we document instance variables this way?
1882 def convert(self, x):
1883 "convert a value into graph coordinates"
1885 def invert(self, v):
1886 "invert a graph coordinate to a axis value"
1888 def setdatarange(self, min, max):
1889 """set the axis data range
1890 - the type of min and max must fit to the axis
1891 - min<max; the axis might be reversed, but this is
1892 expressed internally only
1893 - the axis might not apply this change of the range
1894 (e.g. when the axis range is fixed by the user)
1895 - for invalid parameters (e.g. negativ values at an
1896 logarithmic axis), an exception should be raised"""
1897 # TODO: be more specific about exceptions
1899 def settickrange(self, min, max):
1900 """set the axis tick range
1901 - as before, but for the tick range (whatever this
1902 means for the axis)"""
1904 def getdatarange(self):
1905 """return data range as a tuple (min, max)
1906 - min<max; the axis might be reversed, but this is
1907 expressed internally only"""
1908 # TODO: be more specific about exceptions
1910 def gettickrange(self):
1911 """return tick range as a tuple (min, max)
1912 - as before, but for the tick range"""
1913 # TODO: be more specific about exceptions
1915 def finish(self, axispos, texrunner):
1916 """finishes the axis
1917 - axispos implements _Iaxispos (usually the graph itself
1918 can be used here)
1919 - the finish method creates an axiscanvas, which should be
1920 insertable into the graph to finally paint the axis
1921 - any modification of the axis range should be disabled after
1922 the finish method was called; a possible implementation
1923 would be to raise an error in these methods as soon as an
1924 axiscanvas is available"""
1925 # TODO: be more specific about exceptions
1927 def createlinkaxis(self, **kwargs):
1928 """create a link axis to the axis itself
1929 - typically, a link axis is a axis, which share almost
1930 all properties with the axis it is linked to
1931 - typically, the painter gets replaced by a painter
1932 which doesn't put any text to the axis"""
1935 class _Iaxispos:
1936 """interface definition of axis tick position methods
1937 - these methods are used for the postitioning of the ticks
1938 when painting an axis
1939 - you may try to avoid calling of these methods from each
1940 other to gain speed"""
1942 def baseline(self, x1=None, x2=None, axis=None):
1943 """return the baseline as a path
1944 - x1 is the start position; if not set, the baseline starts
1945 from the beginning of the axis, which might imply a
1946 value outside of the graph coordinate range [0; 1]
1947 - x2 is analogous to x1, but for the end position"""
1949 def vbaseline(self, v1=None, v2=None, axis=None):
1950 """return the baseline as a path
1951 - like baseline, but for graph coordinates"""
1953 def gridline(self, x, axis=None):
1954 "return the gridline as a path for a given position x"
1956 def vgridline(self, v, axis=None):
1957 """return the gridline as a path for a given position v
1958 in graph coordinates"""
1960 def _tickpoint(self, x, axis=None):
1961 """return the position at the baseline as a tuple (x, y) in
1962 postscript points for the position x"""
1964 def tickpoint(self, x, axis=None):
1965 """return the position at the baseline as a tuple (x, y) in
1966 in PyX length for the position x"""
1968 def _vtickpoint(self, v, axis=None):
1969 "like _tickpoint, but for graph coordinates"
1971 def vtickpoint(self, v, axis=None):
1972 "like tickpoint, but for graph coordinates"
1974 def tickdirection(self, x, axis=None):
1975 """return the direction of a tick as a tuple (dx, dy) for the
1976 position x"""
1978 def vtickdirection(self, v, axis=None):
1979 """like tickposition, but for graph coordinates"""
1982 class _axis:
1983 """base implementation a regular axis
1984 - typical usage is to mix-in a linmap or a logmap to
1985 complete the definition"""
1987 def __init__(self, min=None, max=None, reverse=0, divisor=1,
1988 datavmin=None, datavmax=None, tickvmin=0, tickvmax=1,
1989 title=None, painter=axispainter(), texter=defaulttexter(),
1990 density=1, maxworse=2):
1991 """initializes the instance
1992 - min and max fix the axis minimum and maximum, respectively;
1993 they are determined by the data to be plotted, when not fixed
1994 - reverse (boolean) reverses the minimum and the maximum of
1995 the axis
1996 - numerical divisor for the axis partitioning
1997 - datavmin and datavmax are the minimal and maximal graph
1998 coordinate when adjusting the axis to the data range
1999 (likely to be changed in the future)
2000 - as before, but for the tick range
2001 - title is a string containing the axis title
2002 - axispainter is the axis painter (should implement _Ipainter)
2003 - texter is the texter (should implement _Itexter)
2004 - density is a global parameter for the axis paritioning and
2005 axis rating; its default is 1, but the range 0.5 to 2.5 should
2006 be usefull to get less or more ticks by the automatic axis
2007 partitioning
2008 - maxworse is a number of trials with worse tick rating
2009 before giving up (usually it should not be needed to increase
2010 this value; increasing the number will slow down the automatic
2011 axis partitioning considerably)
2012 - note that some methods of this class want to access a
2013 part and a rating attribute of the instance; those
2014 attributes should be initialized by the constructors
2015 of derived classes"""
2016 # TODO: improve datavmin/datavmax/tickvmin/tickvmax
2017 if None not in (min, max) and min > max:
2018 min, max, reverse = max, min, not reverse
2019 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
2021 self.datamin = self.datamax = self.tickmin = self.tickmax = None
2022 if datavmin is None:
2023 if self.fixmin:
2024 self.datavmin = 0
2025 else:
2026 self.datavmin = 0.05
2027 else:
2028 self.datavmin = datavmin
2029 if datavmax is None:
2030 if self.fixmax:
2031 self.datavmax = 1
2032 else:
2033 self.datavmax = 0.95
2034 else:
2035 self.datavmax = datavmax
2036 self.tickvmin = tickvmin
2037 self.tickvmax = tickvmax
2039 self.divisor = divisor
2040 self.title = title
2041 self.painter = painter
2042 self.texter = texter
2043 self.density = density
2044 self.maxworse = maxworse
2045 self.axiscanvas = None
2046 self.canconvert = 0
2047 self.finished = 0
2048 self.__setinternalrange()
2050 def __setinternalrange(self, min=None, max=None):
2051 if not self.fixmin and min is not None and (self.min is None or min < self.min):
2052 self.min = min
2053 if not self.fixmax and max is not None and (self.max is None or max > self.max):
2054 self.max = max
2055 if None not in (self.min, self.max):
2056 min, max, vmin, vmax = self.min, self.max, 0, 1
2057 self.canconvert = 1
2058 self.setbasepoints(((min, vmin), (max, vmax)))
2059 if not self.fixmin:
2060 if self.datamin is not None and self.convert(self.datamin) < self.datavmin:
2061 min, vmin = self.datamin, self.datavmin
2062 self.setbasepoints(((min, vmin), (max, vmax)))
2063 if self.tickmin is not None and self.convert(self.tickmin) < self.tickvmin:
2064 min, vmin = self.tickmin, self.tickvmin
2065 self.setbasepoints(((min, vmin), (max, vmax)))
2066 if not self.fixmax:
2067 if self.datamax is not None and self.convert(self.datamax) > self.datavmax:
2068 max, vmax = self.datamax, self.datavmax
2069 self.setbasepoints(((min, vmin), (max, vmax)))
2070 if self.tickmax is not None and self.convert(self.tickmax) > self.tickvmax:
2071 max, vmax = self.tickmax, self.tickvmax
2072 self.setbasepoints(((min, vmin), (max, vmax)))
2073 if self.reverse:
2074 self.setbasepoints(((min, vmax), (max, vmin)))
2076 def __getinternalrange(self):
2077 return self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax
2079 def __forceinternalrange(self, range):
2080 self.min, self.max, self.datamin, self.datamax, self.tickmin, self.tickmax = range
2081 self.__setinternalrange()
2083 def setdatarange(self, min, max):
2084 if self.axiscanvas is not None:
2085 raise RuntimeError("axis was already finished")
2086 self.datamin, self.datamax = min, max
2087 self.__setinternalrange(min, max)
2089 def settickrange(self, min, max):
2090 if self.axiscanvas is not None:
2091 raise RuntimeError("axis was already finished")
2092 self.tickmin, self.tickmax = min, max
2093 self.__setinternalrange(min, max)
2095 def getdatarange(self):
2096 if self.canconvert:
2097 if self.reverse:
2098 return self.invert(1-self.datavmin), self.invert(1-self.datavmax)
2099 else:
2100 return self.invert(self.datavmin), self.invert(self.datavmax)
2102 def gettickrange(self):
2103 if self.canconvert:
2104 if self.reverse:
2105 return self.invert(1-self.tickvmin), self.invert(1-self.tickvmax)
2106 else:
2107 return self.invert(self.tickvmin), self.invert(self.tickvmax)
2109 def checkfraclist(self, fracs):
2110 "orders a list of fracs, equal entries are not allowed"
2111 if not len(fracs): return []
2112 sorted = list(fracs)
2113 sorted.sort()
2114 last = sorted[0]
2115 for item in sorted[1:]:
2116 if last == item:
2117 raise ValueError("duplicate entry found")
2118 last = item
2119 return sorted
2121 def finish(self, axispos, texrunner):
2122 if self.finished:
2123 return
2124 self.finished = 1
2126 min, max = self.gettickrange()
2127 parter = parterpos = None
2128 if self.part is not None:
2129 self.part = helper.ensurelist(self.part)
2130 for p, i in zip(self.part, xrange(sys.maxint)):
2131 if hasattr(p, "defaultpart"):
2132 if parter is not None:
2133 raise RuntimeError("only one partitioner allowed")
2134 parter = p
2135 parterpos = i
2136 if parter is None:
2137 self.ticks = self.checkfraclist(self.part)
2138 else:
2139 self.part[:parterpos] = self.checkfraclist(self.part[:parterpos])
2140 self.part[parterpos+1:] = self.checkfraclist(self.part[parterpos+1:])
2141 if self.divisor is not None: # XXX workaround for timeaxis
2142 self.ticks = _mergeticklists(
2143 _mergeticklists(self.part[:parterpos],
2144 parter.defaultpart(min/self.divisor,
2145 max/self.divisor,
2146 not self.fixmin,
2147 not self.fixmax)),
2148 self.part[parterpos+1:])
2149 else:
2150 self.ticks = _mergeticklists(
2151 _mergeticklists(self.part[:parterpos],
2152 parter.defaultpart(min,
2153 max,
2154 not self.fixmin,
2155 not self.fixmax)),
2156 self.part[parterpos+1:])
2157 else:
2158 self.ticks = []
2159 # lesspart and morepart can be called after defaultpart;
2160 # this works although some axes may share their autoparting,
2161 # because the axes are processed sequentially
2162 first = 1
2163 worse = 0
2164 while worse < self.maxworse:
2165 if parter is not None:
2166 newticks = parter.lesspart()
2167 if parterpos is not None and newticks is not None:
2168 newticks = _mergeticklists(_mergeticklists(self.part[:parterpos], newticks), self.part[parterpos+1:])
2169 else:
2170 newticks = None
2171 if newticks is not None:
2172 if first:
2173 bestrate = self.rater.ratepart(self, self.ticks, self.density)
2174 variants = [[bestrate, self.ticks]]
2175 first = 0
2176 newrate = self.rater.ratepart(self, newticks, self.density)
2177 variants.append([newrate, newticks])
2178 if newrate < bestrate:
2179 bestrate = newrate
2180 worse = 0
2181 else:
2182 worse += 1
2183 else:
2184 worse += 1
2185 worse = 0
2186 while worse < self.maxworse:
2187 if parter is not None:
2188 newticks = parter.morepart()
2189 if parterpos is not None and newticks is not None:
2190 newticks = _mergeticklists(_mergeticklists(self.part[:parterpos], newticks), self.part[parterpos+1:])
2191 else:
2192 newticks = None
2193 if newticks is not None:
2194 if first:
2195 bestrate = self.rater.ratepart(self, self.ticks, self.density)
2196 variants = [[bestrate, self.ticks]]
2197 first = 0
2198 newrate = self.rater.ratepart(self, newticks, self.density)
2199 variants.append([newrate, newticks])
2200 if newrate < bestrate:
2201 bestrate = newrate
2202 worse = 0
2203 else:
2204 worse += 1
2205 else:
2206 worse += 1
2208 if not first:
2209 variants.sort()
2210 if self.painter is not None:
2211 i = 0
2212 bestrate = None
2213 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
2214 saverange = self.__getinternalrange()
2215 self.ticks = variants[i][1]
2216 if len(self.ticks):
2217 if self.divisor is not None: # XXX workaround for timeaxis
2218 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2219 else:
2220 self.settickrange(self.ticks[0], self.ticks[-1])
2221 ac = axiscanvas(texrunner)
2222 self.texter.labels(self.ticks)
2223 self.painter.paint(axispos, self, ac)
2224 ratelayout = self.rater.ratelayout(ac, self.density)
2225 if ratelayout is not None:
2226 variants[i][0] += ratelayout
2227 variants[i].append(ac)
2228 else:
2229 variants[i][0] = None
2230 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
2231 bestrate = variants[i][0]
2232 self.__forceinternalrange(saverange)
2233 i += 1
2234 if bestrate is None:
2235 raise RuntimeError("no valid axis partitioning found")
2236 variants = [variant for variant in variants[:i] if variant[0] is not None]
2237 variants.sort()
2238 self.ticks = variants[0][1]
2239 if len(self.ticks):
2240 if self.divisor is not None: # XXX workaround for timeaxis
2241 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2242 else:
2243 self.settickrange(self.ticks[0], self.ticks[-1])
2244 self.axiscanvas = variants[0][2]
2245 else:
2246 if len(self.ticks):
2247 if self.divisor is not None: # XXX workaround for timeaxis
2248 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2249 else:
2250 self.settickrange(self.ticks[0], self.ticks[-1])
2251 self.axiscanvas = axiscanvas(texrunner)
2252 else:
2253 if len(self.ticks):
2254 if self.divisor is not None: # XXX workaround for timeaxis
2255 self.settickrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2256 else:
2257 self.settickrange(self.ticks[0], self.ticks[-1])
2258 self.axiscanvas = axiscanvas(texrunner)
2259 self.texter.labels(self.ticks)
2260 self.painter.paint(axispos, self, self.axiscanvas)
2262 def createlinkaxis(self, **args):
2263 return linkaxis(self, **args)
2266 class linaxis(_axis, _linmap):
2267 """implementation of a linear axis"""
2269 __implements__ = _Iaxis
2271 def __init__(self, part=autolinpart(), rater=axisrater(), **args):
2272 """initializes the instance
2273 - the part attribute contains a list of one partitioner
2274 (a partitioner implements _Ipart) or/and some (manually
2275 set) ticks (implementing _Itick); a single entry might
2276 be passed without wrapping it into a list; the partitioner
2277 and the tick instances must fit to the type of the axis
2278 (e.g. they should be valid parameters to the axis convert
2279 method); the ticks and the partitioner results are mixed
2280 by _mergeticklists
2281 - the rater implements _Irater and is used to rate different
2282 tick lists created by the partitioner (after merging with
2283 manully set ticks)
2284 - futher keyword arguments are passed to _axis"""
2285 _axis.__init__(self, **args)
2286 if self.fixmin and self.fixmax:
2287 self.relsize = self.max - self.min
2288 self.part = part
2289 self.rater = rater
2292 class logaxis(_axis, _logmap):
2293 """implementation of a logarithmic axis"""
2295 __implements__ = _Iaxis
2297 def __init__(self, part=autologpart(), rater=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
2298 """initializes the instance
2299 - the part attribute contains a list of one partitioner
2300 (a partitioner implements _Ipart) or/and some (manually
2301 set) ticks (implementing _Itick); a single entry might
2302 be passed without wrapping it into a list; the partitioner
2303 and the tick instances must fit to the type of the axis
2304 (e.g. they should be valid parameters to the axis convert
2305 method); the ticks and the partitioner results are mixed
2306 by _mergeticklists
2307 - the rater implements _Irater and is used to rate different
2308 tick lists created by the partitioner (after merging with
2309 manully set ticks)
2310 - futher keyword arguments are passed to _axis"""
2311 _axis.__init__(self, **args)
2312 if self.fixmin and self.fixmax:
2313 self.relsize = math.log(self.max) - math.log(self.min)
2314 self.part = part
2315 self.rater = rater
2318 class linkaxis:
2319 """a axis linked to an already existing regular axis
2320 - almost all properties of the axis are "copied" from the
2321 axis this axis is linked to
2322 - usually, linked axis are used to create an axis to an
2323 existing axis with different painting properties; linked
2324 axis can be used to plot an axis twice at the opposite
2325 sides of a graphxy or even to share an axis between
2326 different graphs!"""
2328 __implements__ = _Iaxis
2330 def __init__(self, linkedaxis, painter=linkaxispainter()):
2331 """initializes the instance
2332 - it gets a axis this linkaxis is linked to
2333 - it gets a painter to be used for this linked axis"""
2334 self.linkedaxis = linkedaxis
2335 self.painter = painter
2336 self.finished = 0
2338 def __getattr__(self, attr):
2339 """access to unkown attributes are handed over to the
2340 axis this linkaxis is linked to"""
2341 return getattr(self.linkedaxis, attr)
2343 def finish(self, axispos, texrunner):
2344 """finishes the axis
2345 - instead of performing the hole finish process
2346 (paritioning, rating, etc.) just a painter call
2347 is performed"""
2348 if self.finished:
2349 return
2350 self.finished = 1
2351 self.linkedaxis.finish(axispos, texrunner)
2352 self.axiscanvas = axiscanvas(texrunner)
2353 self.painter.paint(axispos, self, self.axiscanvas)
2356 class _subaxispos:
2357 """implementation of the _Iaxispos interface for subaxis
2358 - provides the _Iaxispos interface for axis which contain
2359 several subaxes
2360 - typical usage by mix-in this class into an "main" axis and
2361 calling the painter(s) of the subaxes from the specialized
2362 painter of the "main" axis; the subaxes need to have the
2363 attributes "baseaxis" (reference to the "main" axis) and
2364 "baseaxispos" (reference to the instance implementing the
2365 _Iaxispos interface of the "main" axis)"""
2367 __implements__ = _Iaxispos
2369 def baseline(self, x1=None, x2=None, axis=None):
2370 if x1 is not None:
2371 v1 = axis.convert(x1)
2372 else:
2373 v1 = 0
2374 if x2 is not None:
2375 v2 = axis.convert(x2)
2376 else:
2377 v2 = 1
2378 return axis.baseaxispos.vbaseline(v1, v2, axis=axis.baseaxis)
2380 def vbaseline(self, v1=None, v2=None, axis=None):
2381 if v1 is None:
2382 left = axis.vmin
2383 else:
2384 left = axis.vmin+v1*(axis.vmax-axis.vmin)
2385 if v2 is None:
2386 right = axis.vmax
2387 else:
2388 right = axis.vmin+v2*(axis.vmax-axis.vmin)
2389 return axis.baseaxispos.vbaseline(left, right, axis=axis.baseaxis)
2391 def gridline(self, x, axis=None):
2392 return axis.baseaxispos.vgridline(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2394 def vgridline(self, v, axis=None):
2395 return axis.baseaxispos.vgridline(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2397 def _tickpoint(self, x, axis=None):
2398 return axis.baseaxispos._vtickpoint(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2400 def tickpoint(self, x, axis=None):
2401 return axis.baseaxispos.vtickpoint(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2403 def _vtickpoint(self, v, axis=None):
2404 return axis.baseaxispos._vtickpoint(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2406 def vtickpoint(self, v, axis=None):
2407 return axis.baseaxispos.vtickpoint(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2409 def tickdirection(self, x, axis=None):
2410 return axis.baseaxispos.vtickdirection(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2412 def vtickdirection(self, v, axis=None):
2413 return axis.baseaxispos.vtickdirection(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2416 class splitaxis(_subaxispos):
2417 """implementation of a split axis
2418 - a split axis contains several (sub-)axes with
2419 non-overlapping data ranges -- between these subaxes
2420 the axis is "splitted"
2421 - (just to get sure: a splitaxis can contain other
2422 splitaxes as its subaxes)
2423 - a splitaxis implements the _Iaxispos for its subaxes
2424 by inheritance from _subaxispos"""
2426 __implements__ = _Iaxis, _Iaxispos
2428 def __init__(self, subaxes, splitlist=0.5, splitdist=0.1, relsizesplitdist=1,
2429 title=None, painter=splitaxispainter()):
2430 """initializes the instance
2431 - subaxes is a list of subaxes
2432 - splitlist is a list of graph coordinates, where the splitting
2433 of the main axis should be performed; a single entry (splitting
2434 two axes) doesn't need to be wrapped into a list; if the list
2435 isn't long enough for the subaxes, missing entries are considered
2436 to be None;
2437 - splitdist is the size of the splitting in graph coordinates, when
2438 the associated splitlist entry is not None
2439 - relsizesplitdist: a None entry in splitlist means, that the
2440 position of the splitting should be calculated out of the
2441 relsize values of conrtibuting subaxes (the size of the
2442 splitting is relsizesplitdist in values of the relsize values
2443 of the axes)
2444 - title is the title of the axis as a string
2445 - painter is the painter of the axis; it should be specialized to
2446 the splitaxis
2447 - the relsize of the splitaxis is the sum of the relsizes of the
2448 subaxes including the relsizesplitdist"""
2449 self.subaxes = subaxes
2450 self.painter = painter
2451 self.title = title
2452 self.splitlist = helper.ensurelist(splitlist)
2453 for subaxis in self.subaxes:
2454 subaxis.vmin = None
2455 subaxis.vmax = None
2456 self.subaxes[0].vmin = 0
2457 self.subaxes[0].vminover = None
2458 self.subaxes[-1].vmax = 1
2459 self.subaxes[-1].vmaxover = None
2460 for i in xrange(len(self.splitlist)):
2461 if self.splitlist[i] is not None:
2462 self.subaxes[i].vmax = self.splitlist[i] - 0.5*splitdist
2463 self.subaxes[i].vmaxover = self.splitlist[i]
2464 self.subaxes[i+1].vmin = self.splitlist[i] + 0.5*splitdist
2465 self.subaxes[i+1].vminover = self.splitlist[i]
2466 i = 0
2467 while i < len(self.subaxes):
2468 if self.subaxes[i].vmax is None:
2469 j = relsize = relsize2 = 0
2470 while self.subaxes[i + j].vmax is None:
2471 relsize += self.subaxes[i + j].relsize + relsizesplitdist
2472 j += 1
2473 relsize += self.subaxes[i + j].relsize
2474 vleft = self.subaxes[i].vmin
2475 vright = self.subaxes[i + j].vmax
2476 for k in range(i, i + j):
2477 relsize2 += self.subaxes[k].relsize
2478 self.subaxes[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
2479 relsize2 += 0.5 * relsizesplitdist
2480 self.subaxes[k].vmaxover = self.subaxes[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
2481 relsize2 += 0.5 * relsizesplitdist
2482 self.subaxes[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
2483 if i == 0 and i + j + 1 == len(self.subaxes):
2484 self.relsize = relsize
2485 i += j + 1
2486 else:
2487 i += 1
2489 self.fixmin = self.subaxes[0].fixmin
2490 if self.fixmin:
2491 self.min = self.subaxes[0].min
2492 self.fixmax = self.subaxes[-1].fixmax
2493 if self.fixmax:
2494 self.max = self.subaxes[-1].max
2495 self.finished = 0
2497 def getdatarange(self):
2498 min = self.subaxes[0].getdatarange()
2499 max = self.subaxes[-1].getdatarange()
2500 try:
2501 return min[0], max[1]
2502 except TypeError:
2503 return None
2505 def setdatarange(self, min, max):
2506 self.subaxes[0].setdatarange(min, None)
2507 self.subaxes[-1].setdatarange(None, max)
2509 def gettickrange(self):
2510 min = self.subaxes[0].gettickrange()
2511 max = self.subaxes[-1].gettickrange()
2512 try:
2513 return min[0], max[1]
2514 except TypeError:
2515 return None
2517 def settickrange(self, min, max):
2518 self.subaxes[0].settickrange(min, None)
2519 self.subaxes[-1].settickrange(None, max)
2521 def convert(self, value):
2522 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2523 if value < self.subaxes[0].max:
2524 return self.subaxes[0].vmin + self.subaxes[0].convert(value)*(self.subaxes[0].vmax-self.subaxes[0].vmin)
2525 for axis in self.subaxes[1:-1]:
2526 if value > axis.min and value < axis.max:
2527 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
2528 if value > self.subaxes[-1].min:
2529 return self.subaxes[-1].vmin + self.subaxes[-1].convert(value)*(self.subaxes[-1].vmax-self.subaxes[-1].vmin)
2530 raise ValueError("value couldn't be assigned to a split region")
2532 def vbaseline(self, v1=None, v2=None, axis=None):
2533 if v1 is None:
2534 if axis.baseaxis.painter.breaklinesattrs is None: # XXX undocumented access to painter!?
2535 left = axis.vmin
2536 else:
2537 if axis.vminover is None:
2538 left = None
2539 else:
2540 left = axis.vminover
2541 else:
2542 left = axis.vmin+v1*(axis.vmax-axis.vmin)
2543 if v2 is None:
2544 if axis.baseaxis.painter.breaklinesattrs is None:
2545 right = axis.vmax
2546 else:
2547 if axis.vmaxover is None:
2548 right = None
2549 else:
2550 right = axis.vmaxover
2551 else:
2552 right = axis.vmin+v2*(axis.vmax-axis.vmin)
2553 return axis.baseaxispos.vbaseline(left, right, axis=axis.baseaxis)
2555 def finish(self, axispos, texrunner):
2556 if self.finished:
2557 return
2558 self.finished = 1
2559 for subaxis in self.subaxes:
2560 subaxis.baseaxispos = axispos
2561 subaxis.baseaxis = self
2562 self.axiscanvas = axiscanvas(texrunner)
2563 self.painter.paint(axispos, self, self.axiscanvas)
2565 def createlinkaxis(self, **args):
2566 return linksplitaxis(self, **args)
2569 class linksplitaxis(linkaxis):
2570 """a splitaxis linked to an already existing splitaxis
2571 - inherits the access to a linked axis -- as before,
2572 basically only the painter is replaced
2573 - it must take care of the creation of linked axes of
2574 the subaxes"""
2576 __implements__ = _Iaxis
2578 def __init__(self, linkedaxis, painter=linksplitaxispainter(), subaxispainter=None):
2579 """initializes the instance
2580 - it gets a axis this linkaxis is linked to
2581 - it gets a painter to be used for this linked axis
2582 - it gets a list of painters to be used for the linkaxes
2583 of the subaxes; if None, the createlinkaxis of the subaxes
2584 are called without a painter parameter; if it is not a
2585 list, the subaxispainter is passed as the painter
2586 parameter to all createlinkaxis of the subaxes"""
2587 if subaxispainter is not None:
2588 if helper.issequence(subaxispainter):
2589 if len(linkedaxis.subaxes) != len(subaxispainter):
2590 raise RuntimeError("subaxes and subaxispainter lengths do not fit")
2591 self.subaxes = [a.createlinkaxis(painter=p) for a, p in zip(linkedaxis.subaxes, subaxispainter)]
2592 else:
2593 self.subaxes = [a.createlinkaxis(painter=subaxispainter) for a in linkedaxis.subaxes]
2594 else:
2595 self.subaxes = [a.createlinkaxis() for a in linkedaxis.subaxes]
2596 linkaxis.__init__(self, linkedaxis, painter=painter)
2598 def finish(self, axispos, texrunner):
2599 for subaxis in self.subaxes:
2600 subaxis.baseaxispos = axispos
2601 subaxis.baseaxis = self
2602 linkaxis.finish(self, axispos, texrunner)
2605 class baraxis(_subaxispos):
2606 """implementation of a axis for bar graphs
2607 - a bar axes is different from a splitaxis by the way it
2608 selects its subaxes: the convert method gets a list,
2609 where the first entry is a name selecting a subaxis out
2610 of a list; instead of the term "bar" or "subaxis" the term
2611 "item" will be used here
2612 - the baraxis stores a list of names be identify the items;
2613 the names might be of any time (strings, integers, etc.);
2614 the names can be printed as the titles for the items, but
2615 alternatively the names might be transformed by the texts
2616 dictionary, which maps a name to a text to be used to label
2617 the items in the painter
2618 - usually, there is only one subaxis, which is used as
2619 the subaxis for all items
2620 - alternatively it is also possible to use another baraxis
2621 as a multisubaxis; it is copied via the createsubaxis
2622 method whenever another subaxis is needed (by that a
2623 nested bar axis with a different number of subbars at
2624 each item can be created)
2625 - any axis can be a subaxis of a baraxis; if no subaxis
2626 is specified at all, the baraxis simulates a linear
2627 subaxis with a fixed range of 0 to 1
2628 - a splitaxis implements the _Iaxispos for its subaxes
2629 by inheritance from _subaxispos when the multisubaxis
2630 feature is turned on"""
2632 def __init__(self, subaxis=None, multisubaxis=None, title=None,
2633 dist=0.5, firstdist=None, lastdist=None, names=None,
2634 texts={}, painter=baraxispainter()):
2635 """initialize the instance
2636 - subaxis contains a axis to be used as the subaxis
2637 for all items
2638 - multisubaxis might contain another baraxis instance
2639 to be used to construct a new subaxis for each item;
2640 (by that a nested bar axis with a different number
2641 of subbars at each item can be created)
2642 - only one of subaxis or multisubaxis can be set; if neither
2643 of them is set, the baraxis behaves like having a linaxis
2644 as its subaxis with a fixed range 0 to 1
2645 - the title attribute contains the axis title as a string
2646 - the dist is a relsize to be used as the distance between
2647 the items
2648 - the firstdist and lastdist are the distance before the
2649 first and after the last item, respectively; when set
2650 to None (the default), 0.5*dist is used
2651 - names is a predefined list of names to identify the
2652 items; if set, the name list is fixed
2653 - texts is a dictionary transforming a name to a text in
2654 the painter; if a name isn't found in the dictionary
2655 it gets used itself
2656 - the relsize of the baraxis is the sum of the
2657 relsizes including all distances between the items"""
2658 self.dist = dist
2659 if firstdist is not None:
2660 self.firstdist = firstdist
2661 else:
2662 self.firstdist = 0.5 * dist
2663 if lastdist is not None:
2664 self.lastdist = lastdist
2665 else:
2666 self.lastdist = 0.5 * dist
2667 self.relsizes = None
2668 self.fixnames = 0
2669 self.names = []
2670 for name in helper.ensuresequence(names):
2671 self.setname(name)
2672 self.fixnames = names is not None
2673 self.multisubaxis = multisubaxis
2674 if self.multisubaxis is not None:
2675 if subaxis is not None:
2676 raise RuntimeError("either use subaxis or multisubaxis")
2677 self.subaxis = [self.createsubaxis() for name in self.names]
2678 else:
2679 self.subaxis = subaxis
2680 self.title = title
2681 self.fixnames = 0
2682 self.texts = texts
2683 self.painter = painter
2685 def createsubaxis(self):
2686 return baraxis(subaxis=self.multisubaxis.subaxis,
2687 multisubaxis=self.multisubaxis.multisubaxis,
2688 title=self.multisubaxis.title,
2689 dist=self.multisubaxis.dist,
2690 firstdist=self.multisubaxis.firstdist,
2691 lastdist=self.multisubaxis.lastdist,
2692 names=self.multisubaxis.names,
2693 texts=self.multisubaxis.texts,
2694 painter=self.multisubaxis.painter)
2696 def getdatarange(self):
2697 # TODO: we do not yet have a proper range handling for a baraxis
2698 return None
2700 def gettickrange(self):
2701 # TODO: we do not yet have a proper range handling for a baraxis
2702 raise RuntimeError("range handling for a baraxis is not implemented")
2704 def setdatarange(self):
2705 # TODO: we do not yet have a proper range handling for a baraxis
2706 raise RuntimeError("range handling for a baraxis is not implemented")
2708 def settickrange(self):
2709 # TODO: we do not yet have a proper range handling for a baraxis
2710 raise RuntimeError("range handling for a baraxis is not implemented")
2712 def setname(self, name, *subnames):
2713 """add a name to identify an item at the baraxis
2714 - by using subnames, nested name definitions are
2715 possible
2716 - a style (or the user itself) might use this to
2717 insert new items into a baraxis
2718 - setting self.relsizes to None forces later recalculation"""
2719 if not self.fixnames:
2720 if name not in self.names:
2721 self.relsizes = None
2722 self.names.append(name)
2723 if self.multisubaxis is not None:
2724 self.subaxis.append(self.createsubaxis())
2725 if (not self.fixnames or name in self.names) and len(subnames):
2726 if self.multisubaxis is not None:
2727 if self.subaxis[self.names.index(name)].setname(*subnames):
2728 self.relsizes = None
2729 else:
2730 if self.subaxis.setname(*subnames):
2731 self.relsizes = None
2732 return self.relsizes is not None
2734 def updaterelsizes(self):
2735 # guess what it does: it recalculates relsize attribute
2736 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
2737 self.relsizes[-1] += self.lastdist - self.dist
2738 if self.multisubaxis is not None:
2739 subrelsize = 0
2740 for i in range(1, len(self.relsizes)):
2741 self.subaxis[i-1].updaterelsizes()
2742 subrelsize += self.subaxis[i-1].relsizes[-1]
2743 self.relsizes[i] += subrelsize
2744 else:
2745 if self.subaxis is None:
2746 subrelsize = 1
2747 else:
2748 self.subaxis.updaterelsizes()
2749 subrelsize = self.subaxis.relsizes[-1]
2750 for i in range(1, len(self.relsizes)):
2751 self.relsizes[i] += i * subrelsize
2753 def convert(self, value):
2754 """baraxis convert method
2755 - the value should be a list, where the first entry is
2756 a member of the names (set in the constructor or by the
2757 setname method); this first entry identifies an item in
2758 the baraxis
2759 - following values are passed to the appropriate subaxis
2760 convert method
2761 - when there is no subaxis, the convert method will behave
2762 like having a linaxis from 0 to 1 as subaxis"""
2763 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2764 if not self.relsizes:
2765 self.updaterelsizes()
2766 pos = self.names.index(value[0])
2767 if len(value) == 2:
2768 if self.subaxis is None:
2769 subvalue = value[1]
2770 else:
2771 if self.multisubaxis is not None:
2772 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
2773 else:
2774 subvalue = value[1] * self.subaxis.relsizes[-1]
2775 else:
2776 if self.multisubaxis is not None:
2777 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
2778 else:
2779 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
2780 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
2782 def baseline(self, x1=None, x2=None, axis=None):
2783 "None is returned -> subaxis should not paint any baselines"
2784 # TODO: quick hack ?!
2785 return None
2787 def vbaseline(self, v1=None, v2=None, axis=None):
2788 "None is returned -> subaxis should not paint any baselines"
2789 # TODO: quick hack ?!
2790 return None
2792 def finish(self, axispos, texrunner):
2793 if self.multisubaxis is not None:
2794 for name, subaxis in zip(self.names, self.subaxis):
2795 subaxis.vmin = self.convert((name, 0))
2796 subaxis.vmax = self.convert((name, 1))
2797 subaxis.baseaxispos = axispos
2798 subaxis.baseaxis = self
2799 self.axiscanvas = axiscanvas(texrunner)
2800 self.painter.paint(axispos, self, self.axiscanvas)
2802 def createlinkaxis(self, **args):
2803 return linkbaraxis(self, **args)
2806 class linkbaraxis(linkaxis):
2807 """a baraxis linked to an already existing baraxis
2808 - inherits the access to a linked axis -- as before,
2809 basically only the painter is replaced
2810 - it must take care of the creation of linked axes of
2811 the subaxes"""
2813 __implements__ = _Iaxis
2815 def __init__(self, linkedaxis, painter=linkbaraxispainter()):
2816 """initializes the instance
2817 - it gets a axis this linkaxis is linked to
2818 - it gets a painter to be used for this linked axis"""
2819 linkaxis.__init__(self, linkedaxis, painter=painter)
2821 def finish(self, axispos, texrunner):
2822 if self.multisubaxis is not None:
2823 self.subaxis = [subaxis.createlinkaxis() for subaxis in self.linkedaxis.subaxis]
2824 for subaxis in self.subaxis:
2825 subaxis.baseaxispos = axispos
2826 subaxis.baseaxis = self
2827 elif self.linkedaxis.subaxis is not None:
2828 self.subaxis = self.linkedaxis.subaxis.createlinkaxis()
2829 self.subaxis.baseaxispos = axispos
2830 self.subaxis.baseaxis = self
2831 linkaxis.finish(self, axispos, texrunner)
2834 ################################################################################
2835 # graph key
2836 ################################################################################
2839 # g = graph.graphxy(key=graph.key())
2840 # g.addkey(graph.key(), ...)
2843 class key:
2845 def __init__(self, dist="0.2 cm", pos = "tr", hinside = 1, vinside = 1, hdist="0.6 cm", vdist="0.4 cm",
2846 symbolwidth="0.5 cm", symbolheight="0.25 cm", symbolspace="0.2 cm",
2847 textattrs=textmodule.vshift.mathaxis):
2848 self.dist_str = dist
2849 self.pos = pos
2850 self.hinside = hinside
2851 self.vinside = vinside
2852 self.hdist_str = hdist
2853 self.vdist_str = vdist
2854 self.symbolwidth_str = symbolwidth
2855 self.symbolheight_str = symbolheight
2856 self.symbolspace_str = symbolspace
2857 self.textattrs = textattrs
2858 self.plotinfos = None
2859 if self.pos in ("tr", "rt"):
2860 self.right = 1
2861 self.top = 1
2862 elif self.pos in ("br", "rb"):
2863 self.right = 1
2864 self.top = 0
2865 elif self.pos in ("tl", "lt"):
2866 self.right = 0
2867 self.top = 1
2868 elif self.pos in ("bl", "lb"):
2869 self.right = 0
2870 self.top = 0
2871 else:
2872 raise RuntimeError("invalid pos attribute")
2874 def setplotinfos(self, *plotinfos):
2875 """set the plotinfos to be used in the key
2876 - call it exactly once"""
2877 if self.plotinfos is not None:
2878 raise RuntimeError("setplotinfo is called multiple times")
2879 self.plotinfos = plotinfos
2881 def dolayout(self, graph):
2882 "creates the layout of the key"
2883 self._dist = unit.topt(unit.length(self.dist_str, default_type="v"))
2884 self._hdist = unit.topt(unit.length(self.hdist_str, default_type="v"))
2885 self._vdist = unit.topt(unit.length(self.vdist_str, default_type="v"))
2886 self._symbolwidth = unit.topt(unit.length(self.symbolwidth_str, default_type="v"))
2887 self._symbolheight = unit.topt(unit.length(self.symbolheight_str, default_type="v"))
2888 self._symbolspace = unit.topt(unit.length(self.symbolspace_str, default_type="v"))
2889 self.titles = []
2890 for plotinfo in self.plotinfos:
2891 self.titles.append(graph.texrunner._text(0, 0, plotinfo.data.title, *helper.ensuresequence(self.textattrs)))
2892 box._tile(self.titles, self._dist, 0, -1)
2893 box._linealignequal(self.titles, self._symbolwidth + self._symbolspace, 1, 0)
2895 def bbox(self):
2896 """return a bbox for the key
2897 method should be called after dolayout"""
2898 result = self.titles[0].bbox()
2899 for title in self.titles[1:]:
2900 result = result + title.bbox() + bbox._bbox(0, title.center[1] - 0.5 * self._symbolheight,
2901 0, title.center[1] + 0.5 * self._symbolheight)
2902 return result
2904 def paint(self, c, x, y):
2905 """paint the graph key into a canvas c at the position x and y (in postscript points)
2906 - method should be called after dolayout
2907 - the x, y alignment might be calculated by the graph using:
2908 - the bbox of the key as returned by the keys bbox method
2909 - the attributes _hdist, _vdist, hinside, and vinside of the key
2910 - the dimension and geometry of the graph"""
2911 sc = c.insert(canvas.canvas(trafomodule._translate(x, y)))
2912 for plotinfo, title in zip(self.plotinfos, self.titles):
2913 plotinfo.style.key(sc, 0, -0.5 * self._symbolheight + title.center[1],
2914 self._symbolwidth, self._symbolheight)
2915 sc.insert(title)
2918 ################################################################################
2919 # graph
2920 ################################################################################
2923 class plotinfo:
2925 def __init__(self, data, style):
2926 self.data = data
2927 self.style = style
2931 class graphxy(canvas.canvas):
2933 Names = "x", "y"
2935 class axisposdata:
2937 def __init__(self, type, axispos, tickdirection):
2939 - type == 0: x-axis; type == 1: y-axis
2940 - _axispos is the y or x position of the x-axis or y-axis
2941 in postscript points, respectively
2942 - axispos is analogous to _axispos, but as a PyX length
2943 - dx and dy is the tick direction
2945 self.type = type
2946 self.axispos = axispos
2947 self._axispos = unit.topt(axispos)
2948 self.tickdirection = tickdirection
2950 def clipcanvas(self):
2951 return self.insert(canvas.canvas(canvas.clip(path._rect(self._xpos, self._ypos, self._width, self._height))))
2953 def plot(self, data, style=None):
2954 if self.haslayout:
2955 raise RuntimeError("layout setup was already performed")
2956 if style is None:
2957 if helper.issequence(data):
2958 raise RuntimeError("list plot needs an explicit style")
2959 if self.defaultstyle.has_key(data.defaultstyle):
2960 style = self.defaultstyle[data.defaultstyle].iterate()
2961 else:
2962 style = data.defaultstyle()
2963 self.defaultstyle[data.defaultstyle] = style
2964 plotinfos = []
2965 first = 1
2966 for d in helper.ensuresequence(data):
2967 if not first:
2968 style = style.iterate()
2969 first = 0
2970 if d is not None:
2971 d.setstyle(self, style)
2972 plotinfos.append(plotinfo(d, style))
2973 self.plotinfos.extend(plotinfos)
2974 if helper.issequence(data):
2975 return plotinfos
2976 return plotinfos[0]
2978 def addkey(self, key, *plotinfos):
2979 if self.haslayout:
2980 raise RuntimeError("layout setup was already performed")
2981 self.addkeys.append((key, plotinfos))
2983 def _pos(self, x, y, xaxis=None, yaxis=None):
2984 if xaxis is None:
2985 xaxis = self.axes["x"]
2986 if yaxis is None:
2987 yaxis = self.axes["y"]
2988 return self._xpos+xaxis.convert(x)*self._width, self._ypos+yaxis.convert(y)*self._height
2990 def pos(self, x, y, xaxis=None, yaxis=None):
2991 if xaxis is None:
2992 xaxis = self.axes["x"]
2993 if yaxis is None:
2994 yaxis = self.axes["y"]
2995 return self.xpos+xaxis.convert(x)*self.width, self.ypos+yaxis.convert(y)*self.height
2997 def _vpos(self, vx, vy, xaxis=None, yaxis=None):
2998 if xaxis is None:
2999 xaxis = self.axes["x"]
3000 if yaxis is None:
3001 yaxis = self.axes["y"]
3002 return self._xpos+vx*self._width, self._ypos+vy*self._height
3004 def vpos(self, vx, vy, xaxis=None, yaxis=None):
3005 if xaxis is None:
3006 xaxis = self.axes["x"]
3007 if yaxis is None:
3008 yaxis = self.axes["y"]
3009 return self.xpos+vx*self.width, self.ypos+vy*self.height
3011 def baseline(self, x1=None, x2=None, axis=None):
3012 if x1 is not None:
3013 v1 = axis.convert(x1)
3014 else:
3015 v1 = 0
3016 if x2 is not None:
3017 v2 = axis.convert(x2)
3018 else:
3019 v2 = 1
3020 if axis.axisposdata.type:
3021 return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3022 axis.axisposdata._axispos, self._ypos+v2*self._height)
3023 else:
3024 return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3025 self._xpos+v2*self._width, axis.axisposdata._axispos)
3027 def xbaseline(self, x1=None, x2=None, axis=None):
3028 if axis is None:
3029 axis = self.axes["x"]
3030 if axis.axisposdata.type != 0:
3031 raise RuntimeError("wrong axisposdata.type")
3032 if x1 is not None:
3033 v1 = axis.convert(x1)
3034 else:
3035 v1 = 0
3036 if x2 is not None:
3037 v2 = axis.convert(x2)
3038 else:
3039 v2 = 1
3040 return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3041 self._xpos+v2*self._width, axis.axisposdata._axispos)
3043 def ybaseline(self, x1=None, x2=None, axis=None):
3044 if axis is None:
3045 axis = self.axes["y"]
3046 if axis.axisposdata.type != 1:
3047 raise RuntimeError("wrong axisposdata.type")
3048 if x1 is not None:
3049 v1 = axis.convert(x1)
3050 else:
3051 v1 = 0
3052 if x2 is not None:
3053 v2 = axis.convert(x2)
3054 else:
3055 v2 = 1
3056 return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3057 axis.axisposdata._axispos, self._ypos+v2*self._height)
3059 def vbaseline(self, v1=None, v2=None, axis=None):
3060 if v1 is None:
3061 v1 = 0
3062 if v2 is None:
3063 v2 = 1
3064 if axis.axisposdata.type:
3065 return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3066 axis.axisposdata._axispos, self._ypos+v2*self._height)
3067 else:
3068 return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3069 self._xpos+v2*self._width, axis.axisposdata._axispos)
3071 def vxbaseline(self, v1=None, v2=None, axis=None):
3072 if axis is None:
3073 axis = self.axes["x"]
3074 if axis.axisposdata.type != 0:
3075 raise RuntimeError("wrong axisposdata.type")
3076 if v1 is None:
3077 v1 = 0
3078 if v2 is None:
3079 v2 = 1
3080 return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3081 self._xpos+v2*self._width, axis.axisposdata._axispos)
3083 def vybaseline(self, v1=None, v2=None, axis=None):
3084 if axis is None:
3085 axis = self.axes["y"]
3086 if axis.axisposdata.type != 1:
3087 raise RuntimeError("wrong axisposdata.type")
3088 if v1 is None:
3089 v1 = 0
3090 if v2 is None:
3091 v2 = 1
3092 return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3093 axis.axisposdata._axispos, self._ypos+v2*self._height)
3095 def gridline(self, x, axis=None):
3096 v = axis.convert(x)
3097 if axis.axisposdata.type:
3098 return path._line(self._xpos, self._ypos+v*self._height,
3099 self._xpos+self._width, self._ypos+v*self._height)
3100 else:
3101 return path._line(self._xpos+v*self._width, self._ypos,
3102 self._xpos+v*self._width, self._ypos+self._height)
3104 def xgridline(self, x, axis=None):
3105 if axis is None:
3106 axis = self.axes["x"]
3107 v = axis.convert(x)
3108 if axis.axisposdata.type != 0:
3109 raise RuntimeError("wrong axisposdata.type")
3110 return path._line(self._xpos+v*self._width, self._ypos,
3111 self._xpos+v*self._width, self._ypos+self._height)
3113 def ygridline(self, x, axis=None):
3114 if axis is None:
3115 axis = self.axes["y"]
3116 v = axis.convert(x)
3117 if axis.axisposdata.type != 1:
3118 raise RuntimeError("wrong axisposdata.type")
3119 return path._line(self._xpos, self._ypos+v*self._height,
3120 self._xpos+self._width, self._ypos+v*self._height)
3122 def vgridline(self, v, axis=None):
3123 if axis.axisposdata.type:
3124 return path._line(self._xpos, self._ypos+v*self._height,
3125 self._xpos+self._width, self._ypos+v*self._height)
3126 else:
3127 return path._line(self._xpos+v*self._width, self._ypos,
3128 self._xpos+v*self._width, self._ypos+self._height)
3130 def vxgridline(self, v, axis=None):
3131 if axis is None:
3132 axis = self.axes["x"]
3133 if axis.axisposdata.type != 0:
3134 raise RuntimeError("wrong axisposdata.type")
3135 return path._line(self._xpos+v*self._width, self._ypos,
3136 self._xpos+v*self._width, self._ypos+self._height)
3138 def vygridline(self, v, axis=None):
3139 if axis is None:
3140 axis = self.axes["y"]
3141 if axis.axisposdata.type != 1:
3142 raise RuntimeError("wrong axisposdata.type")
3143 return path._line(self._xpos, self._ypos+v*self._height,
3144 self._xpos+self._width, self._ypos+v*self._height)
3146 def _tickpoint(self, x, axis=None):
3147 if axis.axisposdata.type:
3148 return axis.axisposdata._axispos, self._ypos+axis.convert(x)*self._height
3149 else:
3150 return self._xpos+axis.convert(x)*self._width, axis.axisposdata._axispos
3152 def _xtickpoint(self, x, axis=None):
3153 if axis is None:
3154 axis = self.axes["x"]
3155 if axis.axisposdata.type != 0:
3156 raise RuntimeError("wrong axisposdata.type")
3157 return self._xpos+axis.convert(x)*self._width, axis.axisposdata._axispos
3159 def _ytickpoint(self, x, axis=None):
3160 if axis is None:
3161 axis = self.axes["y"]
3162 if axis.axisposdata.type != 1:
3163 raise RuntimeError("wrong axisposdata.type")
3164 return axis.axisposdata._axispos, self._ypos+axis.convert(x)*self._height
3166 def tickpoint(self, x, axis=None):
3167 if axis.axisposdata.type:
3168 return axis.axisposdata.axispos, self.ypos+axis.convert(x)*self.height
3169 else:
3170 return self.xpos+axis.convert(x)*self.width, axis.axisposdata.axispos
3172 def xtickpoint(self, x, axis=None):
3173 if axis is None:
3174 axis = self.axes["x"]
3175 if axis.axisposdata.type != 0:
3176 raise RuntimeError("wrong axisposdata.type")
3177 return self.xpos+axis.convert(x)*self.width, axis.axisposdata.axispos
3179 def ytickpoint(self, x, axis=None):
3180 if axis is None:
3181 axis = self.axes["y"]
3182 if axis.axisposdata.type != 1:
3183 raise RuntimeError("wrong axisposdata.type")
3184 return axis.axisposdata.axispos, self.ypos+axis.convert(x)*self.height
3186 def _vtickpoint(self, v, axis=None):
3187 if axis.axisposdata.type:
3188 return axis.axisposdata._axispos, self._ypos+v*self._height
3189 else:
3190 return self._xpos+v*self._width, axis.axisposdata._axispos
3192 def _vxtickpoint(self, v, axis=None):
3193 if axis is None:
3194 axis = self.axes["x"]
3195 if axis.axisposdata.type != 0:
3196 raise RuntimeError("wrong axisposdata.type")
3197 return self._xpos+v*self._width, axis.axisposdata._axispos
3199 def _vytickpoint(self, v, axis=None):
3200 if axis is None:
3201 axis = self.axes["y"]
3202 if axis.axisposdata.type != 1:
3203 raise RuntimeError("wrong axisposdata.type")
3204 return axis.axisposdata._axispos, self._ypos+v*self._height
3206 def vtickpoint(self, v, axis=None):
3207 if axis.axisposdata.type:
3208 return axis.axisposdata.axispos, self.ypos+v*self.height
3209 else:
3210 return self.xpos+v*self.width, axis.axisposdata.axispos
3212 def vxtickpoint(self, v, axis=None):
3213 if axis is None:
3214 axis = self.axes["x"]
3215 if axis.axisposdata.type != 0:
3216 raise RuntimeError("wrong axisposdata.type")
3217 return self.xpos+v*self.width, axis.axisposdata.axispos
3219 def vytickpoint(self, v, axis=None):
3220 if axis is None:
3221 axis = self.axes["y"]
3222 if axis.axisposdata.type != 1:
3223 raise RuntimeError("wrong axisposdata.type")
3224 return axis.axisposdata.axispos, self.ypos+v*self.height
3226 def tickdirection(self, x, axis=None):
3227 return axis.axisposdata.tickdirection
3229 def xtickdirection(self, x, axis=None):
3230 if axis is None:
3231 axis = self.axes["x"]
3232 if axis.axisposdata.type != 0:
3233 raise RuntimeError("wrong axisposdata.type")
3234 return axis.axisposdata.tickdirection
3236 def ytickdirection(self, x, axis=None):
3237 if axis is None:
3238 axis = self.axes["y"]
3239 if axis.axisposdata.type != 1:
3240 raise RuntimeError("wrong axisposdata.type")
3241 return axis.axisposdata.tickdirection
3243 def vtickdirection(self, v, axis=None):
3244 return axis.axisposdata.tickdirection
3246 def vxtickdirection(self, v, axis=None):
3247 if axis is None:
3248 axis = self.axes["x"]
3249 if axis.axisposdata.type != 0:
3250 raise RuntimeError("wrong axisposdata.type")
3251 return axis.axisposdata.tickdirection
3253 def vytickdirection(self, v, axis=None):
3254 if axis is None:
3255 axis = self.axes["y"]
3256 if axis.axisposdata.type != 1:
3257 raise RuntimeError("wrong axisposdata.type")
3258 return axis.axisposdata.tickdirection
3260 def _addpos(self, x, y, dx, dy):
3261 return x+dx, y+dy
3263 def _connect(self, x1, y1, x2, y2):
3264 return path._lineto(x2, y2)
3266 def keynum(self, key):
3267 try:
3268 while key[0] in string.letters:
3269 key = key[1:]
3270 return int(key)
3271 except IndexError:
3272 return 1
3274 def gatherranges(self):
3275 ranges = {}
3276 for plotinfo in self.plotinfos:
3277 pdranges = plotinfo.data.getranges()
3278 if pdranges is not None:
3279 for key in pdranges.keys():
3280 if key not in ranges.keys():
3281 ranges[key] = pdranges[key]
3282 else:
3283 ranges[key] = (min(ranges[key][0], pdranges[key][0]),
3284 max(ranges[key][1], pdranges[key][1]))
3285 # known ranges are also set as ranges for the axes
3286 for key, axis in self.axes.items():
3287 if key in ranges.keys():
3288 axis.setdatarange(*ranges[key])
3289 ranges[key] = axis.getdatarange()
3290 if ranges[key] is None:
3291 del ranges[key]
3292 return ranges
3294 def removedomethod(self, method):
3295 hadmethod = 0
3296 while 1:
3297 try:
3298 self.domethods.remove(method)
3299 hadmethod = 1
3300 except ValueError:
3301 return hadmethod
3303 def dolayout(self):
3304 if not self.removedomethod(self.dolayout): return
3305 self.haslayout = 1
3306 # create list of ranges
3307 # 1. gather ranges
3308 ranges = self.gatherranges()
3309 # 2. calculate additional ranges out of known ranges
3310 for plotinfo in self.plotinfos:
3311 plotinfo.data.setranges(ranges)
3312 # 3. gather ranges again
3313 self.gatherranges()
3314 # do the layout for all axes
3315 axesdist = unit.length(self.axesdist_str, default_type="v")
3316 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3317 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3318 xaxisextents = [0, 0]
3319 yaxisextents = [0, 0]
3320 needxaxisdist = [0, 0]
3321 needyaxisdist = [0, 0]
3322 items = list(self.axes.items())
3323 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3324 for key, axis in items:
3325 num = self.keynum(key)
3326 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3327 num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3328 if XPattern.match(key):
3329 if needxaxisdist[num2]:
3330 xaxisextents[num2] += axesdist
3331 axis.axisposdata = self.axisposdata(0, self.ypos + num2*self.height + num3*xaxisextents[num2], (0, num3))
3332 elif YPattern.match(key):
3333 if needyaxisdist[num2]:
3334 yaxisextents[num2] += axesdist
3335 axis.axisposdata = self.axisposdata(1, self.xpos + num2*self.width + num3*yaxisextents[num2], (num3, 0))
3336 else:
3337 raise ValueError("Axis key '%s' not allowed" % key)
3338 axis.finish(self, self.texrunner)
3339 if XPattern.match(key):
3340 xaxisextents[num2] += axis.axiscanvas.extent
3341 needxaxisdist[num2] = 1
3342 if YPattern.match(key):
3343 yaxisextents[num2] += axis.axiscanvas.extent
3344 needyaxisdist[num2] = 1
3346 def dobackground(self):
3347 self.dolayout()
3348 if not self.removedomethod(self.dobackground): return
3349 if self.backgroundattrs is not None:
3350 self.draw(path._rect(self._xpos, self._ypos, self._width, self._height),
3351 *helper.ensuresequence(self.backgroundattrs))
3353 def doaxes(self):
3354 self.dolayout()
3355 if not self.removedomethod(self.doaxes): return
3356 for axis in self.axes.values():
3357 self.insert(axis.axiscanvas)
3359 def dodata(self):
3360 self.dolayout()
3361 if not self.removedomethod(self.dodata): return
3362 for plotinfo in self.plotinfos:
3363 plotinfo.data.draw(self)
3365 def _dokey(self, key, *plotinfos):
3366 key.setplotinfos(*plotinfos)
3367 key.dolayout(self)
3368 bbox = key.bbox()
3369 if key.right:
3370 if key.hinside:
3371 x = self._xpos + self._width - bbox.urx - key._hdist
3372 else:
3373 x = self._xpos + self._width - bbox.llx + key._hdist
3374 else:
3375 if key.hinside:
3376 x = self._xpos - bbox.llx + key._hdist
3377 else:
3378 x = self._xpos - bbox.urx - key._hdist
3379 if key.top:
3380 if key.vinside:
3381 y = self._ypos + self._height - bbox.ury - key._vdist
3382 else:
3383 y = self._ypos + self._height - bbox.lly + key._vdist
3384 else:
3385 if key.vinside:
3386 y = self._ypos - bbox.lly + key._vdist
3387 else:
3388 y = self._ypos - bbox.ury - key._vdist
3389 key.paint(self, x, y)
3391 def dokey(self):
3392 self.dolayout()
3393 if not self.removedomethod(self.dokey): return
3394 if self.key is not None:
3395 self._dokey(self.key, *self.plotinfos)
3396 for key, plotinfos in self.addkeys:
3397 self._dokey(key, *plotinfos)
3399 def finish(self):
3400 while len(self.domethods):
3401 self.domethods[0]()
3403 def initwidthheight(self, width, height, ratio):
3404 if (width is not None) and (height is None):
3405 self.width = unit.length(width)
3406 self.height = (1.0/ratio) * self.width
3407 elif (height is not None) and (width is None):
3408 self.height = unit.length(height)
3409 self.width = ratio * self.height
3410 else:
3411 self.width = unit.length(width)
3412 self.height = unit.length(height)
3413 self._width = unit.topt(self.width)
3414 self._height = unit.topt(self.height)
3415 if self._width <= 0: raise ValueError("width <= 0")
3416 if self._height <= 0: raise ValueError("height <= 0")
3418 def initaxes(self, axes, addlinkaxes=0):
3419 for key in self.Names:
3420 if not axes.has_key(key):
3421 axes[key] = linaxis()
3422 elif axes[key] is None:
3423 del axes[key]
3424 if addlinkaxes:
3425 if not axes.has_key(key + "2") and axes.has_key(key):
3426 axes[key + "2"] = axes[key].createlinkaxis()
3427 elif axes[key + "2"] is None:
3428 del axes[key + "2"]
3429 self.axes = axes
3431 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
3432 key=None, backgroundattrs=None, axesdist="0.8 cm", **axes):
3433 canvas.canvas.__init__(self)
3434 self.xpos = unit.length(xpos)
3435 self.ypos = unit.length(ypos)
3436 self._xpos = unit.topt(self.xpos)
3437 self._ypos = unit.topt(self.ypos)
3438 self.initwidthheight(width, height, ratio)
3439 self.initaxes(axes, 1)
3440 self.key = key
3441 self.backgroundattrs = backgroundattrs
3442 self.axesdist_str = axesdist
3443 self.plotinfos = []
3444 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata, self.dokey]
3445 self.haslayout = 0
3446 self.defaultstyle = {}
3447 self.addkeys = []
3449 def bbox(self):
3450 self.finish()
3451 return canvas.canvas.bbox(self)
3453 def write(self, file):
3454 self.finish()
3455 canvas.canvas.write(self, file)
3459 # some thoughts, but deferred right now
3461 # class graphxyz(graphxy):
3463 # Names = "x", "y", "z"
3465 # def _vxtickpoint(self, axis, v):
3466 # return self._vpos(v, axis.vypos, axis.vzpos)
3468 # def _vytickpoint(self, axis, v):
3469 # return self._vpos(axis.vxpos, v, axis.vzpos)
3471 # def _vztickpoint(self, axis, v):
3472 # return self._vpos(axis.vxpos, axis.vypos, v)
3474 # def vxtickdirection(self, axis, v):
3475 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
3476 # x2, y2 = self._vpos(v, 0.5, 0)
3477 # dx, dy = x1 - x2, y1 - y2
3478 # norm = math.sqrt(dx*dx + dy*dy)
3479 # return dx/norm, dy/norm
3481 # def vytickdirection(self, axis, v):
3482 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
3483 # x2, y2 = self._vpos(0.5, v, 0)
3484 # dx, dy = x1 - x2, y1 - y2
3485 # norm = math.sqrt(dx*dx + dy*dy)
3486 # return dx/norm, dy/norm
3488 # def vztickdirection(self, axis, v):
3489 # return -1, 0
3490 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
3491 # x2, y2 = self._vpos(0.5, 0.5, v)
3492 # dx, dy = x1 - x2, y1 - y2
3493 # norm = math.sqrt(dx*dx + dy*dy)
3494 # return dx/norm, dy/norm
3496 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3497 # if xaxis is None: xaxis = self.axes["x"]
3498 # if yaxis is None: yaxis = self.axes["y"]
3499 # if zaxis is None: zaxis = self.axes["z"]
3500 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3502 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3503 # if xaxis is None: xaxis = self.axes["x"]
3504 # if yaxis is None: yaxis = self.axes["y"]
3505 # if zaxis is None: zaxis = self.axes["z"]
3506 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3508 # def _vpos(self, vx, vy, vz):
3509 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
3510 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
3511 # + self.a[2]*self.b[0]*(y-self.eye[1])
3512 # + self.a[1]*self.b[2]*(x-self.eye[0])
3513 # - self.a[2]*self.b[1]*(x-self.eye[0])
3514 # - self.a[0]*self.b[2]*(y-self.eye[1])
3515 # - self.a[1]*self.b[0]*(z-self.eye[2]))
3516 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
3517 # + self.eye[2]*self.b[0]*(y-self.eye[1])
3518 # + self.eye[1]*self.b[2]*(x-self.eye[0])
3519 # - self.eye[2]*self.b[1]*(x-self.eye[0])
3520 # - self.eye[0]*self.b[2]*(y-self.eye[1])
3521 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
3522 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
3523 # + self.a[2]*self.eye[0]*(y-self.eye[1])
3524 # + self.a[1]*self.eye[2]*(x-self.eye[0])
3525 # - self.a[2]*self.eye[1]*(x-self.eye[0])
3526 # - self.a[0]*self.eye[2]*(y-self.eye[1])
3527 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
3528 # return da/d0 + self._xpos, db/d0 + self._ypos
3530 # def vpos(self, vx, vy, vz):
3531 # tx, ty = self._vpos(vx, vy, vz)
3532 # return unit.t_pt(tx), unit.t_pt(ty)
3534 # def xbaseline(self, axis, x1, x2, xaxis=None):
3535 # if xaxis is None: xaxis = self.axes["x"]
3536 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2))
3538 # def ybaseline(self, axis, y1, y2, yaxis=None):
3539 # if yaxis is None: yaxis = self.axes["y"]
3540 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2))
3542 # def zbaseline(self, axis, z1, z2, zaxis=None):
3543 # if zaxis is None: zaxis = self.axes["z"]
3544 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2))
3546 # def vxbaseline(self, axis, v1, v2):
3547 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
3548 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
3549 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
3550 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
3552 # def vybaseline(self, axis, v1, v2):
3553 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
3554 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
3555 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
3556 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
3558 # def vzbaseline(self, axis, v1, v2):
3559 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
3560 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
3561 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
3562 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
3564 # def xgridpath(self, x, xaxis=None):
3565 # assert 0
3566 # if xaxis is None: xaxis = self.axes["x"]
3567 # v = xaxis.convert(x)
3568 # return path._line(self._xpos+v*self._width, self._ypos,
3569 # self._xpos+v*self._width, self._ypos+self._height)
3571 # def ygridpath(self, y, yaxis=None):
3572 # assert 0
3573 # if yaxis is None: yaxis = self.axes["y"]
3574 # v = yaxis.convert(y)
3575 # return path._line(self._xpos, self._ypos+v*self._height,
3576 # self._xpos+self._width, self._ypos+v*self._height)
3578 # def zgridpath(self, z, zaxis=None):
3579 # assert 0
3580 # if zaxis is None: zaxis = self.axes["z"]
3581 # v = zaxis.convert(z)
3582 # return path._line(self._xpos, self._zpos+v*self._height,
3583 # self._xpos+self._width, self._zpos+v*self._height)
3585 # def vxgridpath(self, v):
3586 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
3587 # path._lineto(*self._vpos(v, 0, 1)),
3588 # path._lineto(*self._vpos(v, 1, 1)),
3589 # path._lineto(*self._vpos(v, 1, 0)),
3590 # path.closepath())
3592 # def vygridpath(self, v):
3593 # return path.path(path._moveto(*self._vpos(0, v, 0)),
3594 # path._lineto(*self._vpos(0, v, 1)),
3595 # path._lineto(*self._vpos(1, v, 1)),
3596 # path._lineto(*self._vpos(1, v, 0)),
3597 # path.closepath())
3599 # def vzgridpath(self, v):
3600 # return path.path(path._moveto(*self._vpos(0, 0, v)),
3601 # path._lineto(*self._vpos(0, 1, v)),
3602 # path._lineto(*self._vpos(1, 1, v)),
3603 # path._lineto(*self._vpos(1, 0, v)),
3604 # path.closepath())
3606 # def _addpos(self, x, y, dx, dy):
3607 # assert 0
3608 # return x+dx, y+dy
3610 # def _connect(self, x1, y1, x2, y2):
3611 # assert 0
3612 # return path._lineto(x2, y2)
3614 # def doaxes(self):
3615 # self.dolayout()
3616 # if not self.removedomethod(self.doaxes): return
3617 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
3618 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3619 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3620 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[2])
3621 # items = list(self.axes.items())
3622 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3623 # for key, axis in items:
3624 # num = self.keynum(key)
3625 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3626 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3627 # if XPattern.match(key):
3628 # axis.vypos = 0
3629 # axis.vzpos = 0
3630 # axis._vtickpoint = self._vxtickpoint
3631 # axis.vgridpath = self.vxgridpath
3632 # axis.vbaseline = self.vxbaseline
3633 # axis.vtickdirection = self.vxtickdirection
3634 # elif YPattern.match(key):
3635 # axis.vxpos = 0
3636 # axis.vzpos = 0
3637 # axis._vtickpoint = self._vytickpoint
3638 # axis.vgridpath = self.vygridpath
3639 # axis.vbaseline = self.vybaseline
3640 # axis.vtickdirection = self.vytickdirection
3641 # elif ZPattern.match(key):
3642 # axis.vxpos = 0
3643 # axis.vypos = 0
3644 # axis._vtickpoint = self._vztickpoint
3645 # axis.vgridpath = self.vzgridpath
3646 # axis.vbaseline = self.vzbaseline
3647 # axis.vtickdirection = self.vztickdirection
3648 # else:
3649 # raise ValueError("Axis key '%s' not allowed" % key)
3650 # if axis.painter is not None:
3651 # axis.dopaint(self)
3652 # # if XPattern.match(key):
3653 # # self._xaxisextents[num2] += axis._extent
3654 # # needxaxisdist[num2] = 1
3655 # # if YPattern.match(key):
3656 # # self._yaxisextents[num2] += axis._extent
3657 # # needyaxisdist[num2] = 1
3659 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
3660 # phi=30, theta=30, distance=1,
3661 # backgroundattrs=None, axesdist="0.8 cm", **axes):
3662 # canvas.canvas.__init__(self)
3663 # self.tex = tex
3664 # self.xpos = xpos
3665 # self.ypos = ypos
3666 # self._xpos = unit.topt(xpos)
3667 # self._ypos = unit.topt(ypos)
3668 # self._width = unit.topt(width)
3669 # self._height = unit.topt(height)
3670 # self._depth = unit.topt(depth)
3671 # self.width = width
3672 # self.height = height
3673 # self.depth = depth
3674 # if self._width <= 0: raise ValueError("width < 0")
3675 # if self._height <= 0: raise ValueError("height < 0")
3676 # if self._depth <= 0: raise ValueError("height < 0")
3677 # self._distance = distance*math.sqrt(self._width*self._width+
3678 # self._height*self._height+
3679 # self._depth*self._depth)
3680 # phi *= -math.pi/180
3681 # theta *= math.pi/180
3682 # self.a = (-math.sin(phi), math.cos(phi), 0)
3683 # self.b = (-math.cos(phi)*math.sin(theta),
3684 # -math.sin(phi)*math.sin(theta),
3685 # math.cos(theta))
3686 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
3687 # self._distance*math.sin(phi)*math.cos(theta),
3688 # self._distance*math.sin(theta))
3689 # self.initaxes(axes)
3690 # self.axesdist_str = axesdist
3691 # self.backgroundattrs = backgroundattrs
3693 # self.data = []
3694 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
3695 # self.haslayout = 0
3696 # self.defaultstyle = {}
3698 # def bbox(self):
3699 # self.finish()
3700 # return bbox._bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
3703 ################################################################################
3704 # attr changers
3705 ################################################################################
3708 #class _Ichangeattr:
3709 # """attribute changer
3710 # is an iterator for attributes where an attribute
3711 # is not refered by just a number (like for a sequence),
3712 # but also by the number of attributes requested
3713 # by calls of the next method (like for an color palette)
3714 # (you should ensure to call all needed next before the attr)
3716 # the attribute itself is implemented by overloading the _attr method"""
3718 # def attr(self):
3719 # "get an attribute"
3721 # def next(self):
3722 # "get an attribute changer for the next attribute"
3725 class _changeattr: pass
3728 class changeattr(_changeattr):
3730 def __init__(self):
3731 self.counter = 1
3733 def getattr(self):
3734 return self.attr(0)
3736 def iterate(self):
3737 newindex = self.counter
3738 self.counter += 1
3739 return refattr(self, newindex)
3742 class refattr(_changeattr):
3744 def __init__(self, ref, index):
3745 self.ref = ref
3746 self.index = index
3748 def getattr(self):
3749 return self.ref.attr(self.index)
3751 def iterate(self):
3752 return self.ref.iterate()
3755 # helper routines for a using attrs
3757 def _getattr(attr):
3758 "get attr out of a attr/changeattr"
3759 if isinstance(attr, _changeattr):
3760 return attr.getattr()
3761 return attr
3764 def _getattrs(attrs):
3765 "get attrs out of a list of attr/changeattr"
3766 if attrs is not None:
3767 result = []
3768 for attr in helper.ensuresequence(attrs):
3769 if isinstance(attr, _changeattr):
3770 attr = attr.getattr()
3771 if attr is not None:
3772 result.append(attr)
3773 if len(result) or not len(attrs):
3774 return result
3777 def _iterateattr(attr):
3778 "perform next to a attr/changeattr"
3779 if isinstance(attr, _changeattr):
3780 return attr.iterate()
3781 return attr
3784 def _iterateattrs(attrs):
3785 "perform next to a list of attr/changeattr"
3786 if attrs is not None:
3787 result = []
3788 for attr in helper.ensuresequence(attrs):
3789 if isinstance(attr, _changeattr):
3790 result.append(attr.iterate())
3791 else:
3792 result.append(attr)
3793 return result
3796 class changecolor(changeattr):
3798 def __init__(self, palette):
3799 changeattr.__init__(self)
3800 self.palette = palette
3802 def attr(self, index):
3803 if self.counter != 1:
3804 return self.palette.getcolor(index/float(self.counter-1))
3805 else:
3806 return self.palette.getcolor(0)
3809 class _changecolorgray(changecolor):
3811 def __init__(self, palette=color.palette.Gray):
3812 changecolor.__init__(self, palette)
3814 _changecolorgrey = _changecolorgray
3817 class _changecolorreversegray(changecolor):
3819 def __init__(self, palette=color.palette.ReverseGray):
3820 changecolor.__init__(self, palette)
3822 _changecolorreversegrey = _changecolorreversegray
3825 class _changecolorredblack(changecolor):
3827 def __init__(self, palette=color.palette.RedBlack):
3828 changecolor.__init__(self, palette)
3831 class _changecolorblackred(changecolor):
3833 def __init__(self, palette=color.palette.BlackRed):
3834 changecolor.__init__(self, palette)
3837 class _changecolorredwhite(changecolor):
3839 def __init__(self, palette=color.palette.RedWhite):
3840 changecolor.__init__(self, palette)
3843 class _changecolorwhitered(changecolor):
3845 def __init__(self, palette=color.palette.WhiteRed):
3846 changecolor.__init__(self, palette)
3849 class _changecolorgreenblack(changecolor):
3851 def __init__(self, palette=color.palette.GreenBlack):
3852 changecolor.__init__(self, palette)
3855 class _changecolorblackgreen(changecolor):
3857 def __init__(self, palette=color.palette.BlackGreen):
3858 changecolor.__init__(self, palette)
3861 class _changecolorgreenwhite(changecolor):
3863 def __init__(self, palette=color.palette.GreenWhite):
3864 changecolor.__init__(self, palette)
3867 class _changecolorwhitegreen(changecolor):
3869 def __init__(self, palette=color.palette.WhiteGreen):
3870 changecolor.__init__(self, palette)
3873 class _changecolorblueblack(changecolor):
3875 def __init__(self, palette=color.palette.BlueBlack):
3876 changecolor.__init__(self, palette)
3879 class _changecolorblackblue(changecolor):
3881 def __init__(self, palette=color.palette.BlackBlue):
3882 changecolor.__init__(self, palette)
3885 class _changecolorbluewhite(changecolor):
3887 def __init__(self, palette=color.palette.BlueWhite):
3888 changecolor.__init__(self, palette)
3891 class _changecolorwhiteblue(changecolor):
3893 def __init__(self, palette=color.palette.WhiteBlue):
3894 changecolor.__init__(self, palette)
3897 class _changecolorredgreen(changecolor):
3899 def __init__(self, palette=color.palette.RedGreen):
3900 changecolor.__init__(self, palette)
3903 class _changecolorredblue(changecolor):
3905 def __init__(self, palette=color.palette.RedBlue):
3906 changecolor.__init__(self, palette)
3909 class _changecolorgreenred(changecolor):
3911 def __init__(self, palette=color.palette.GreenRed):
3912 changecolor.__init__(self, palette)
3915 class _changecolorgreenblue(changecolor):
3917 def __init__(self, palette=color.palette.GreenBlue):
3918 changecolor.__init__(self, palette)
3921 class _changecolorbluered(changecolor):
3923 def __init__(self, palette=color.palette.BlueRed):
3924 changecolor.__init__(self, palette)
3927 class _changecolorbluegreen(changecolor):
3929 def __init__(self, palette=color.palette.BlueGreen):
3930 changecolor.__init__(self, palette)
3933 class _changecolorrainbow(changecolor):
3935 def __init__(self, palette=color.palette.Rainbow):
3936 changecolor.__init__(self, palette)
3939 class _changecolorreverserainbow(changecolor):
3941 def __init__(self, palette=color.palette.ReverseRainbow):
3942 changecolor.__init__(self, palette)
3945 class _changecolorhue(changecolor):
3947 def __init__(self, palette=color.palette.Hue):
3948 changecolor.__init__(self, palette)
3951 class _changecolorreversehue(changecolor):
3953 def __init__(self, palette=color.palette.ReverseHue):
3954 changecolor.__init__(self, palette)
3957 changecolor.Gray = _changecolorgray
3958 changecolor.Grey = _changecolorgrey
3959 changecolor.Reversegray = _changecolorreversegray
3960 changecolor.Reversegrey = _changecolorreversegrey
3961 changecolor.RedBlack = _changecolorredblack
3962 changecolor.BlackRed = _changecolorblackred
3963 changecolor.RedWhite = _changecolorredwhite
3964 changecolor.WhiteRed = _changecolorwhitered
3965 changecolor.GreenBlack = _changecolorgreenblack
3966 changecolor.BlackGreen = _changecolorblackgreen
3967 changecolor.GreenWhite = _changecolorgreenwhite
3968 changecolor.WhiteGreen = _changecolorwhitegreen
3969 changecolor.BlueBlack = _changecolorblueblack
3970 changecolor.BlackBlue = _changecolorblackblue
3971 changecolor.BlueWhite = _changecolorbluewhite
3972 changecolor.WhiteBlue = _changecolorwhiteblue
3973 changecolor.RedGreen = _changecolorredgreen
3974 changecolor.RedBlue = _changecolorredblue
3975 changecolor.GreenRed = _changecolorgreenred
3976 changecolor.GreenBlue = _changecolorgreenblue
3977 changecolor.BlueRed = _changecolorbluered
3978 changecolor.BlueGreen = _changecolorbluegreen
3979 changecolor.Rainbow = _changecolorrainbow
3980 changecolor.ReverseRainbow = _changecolorreverserainbow
3981 changecolor.Hue = _changecolorhue
3982 changecolor.ReverseHue = _changecolorreversehue
3985 class changesequence(changeattr):
3986 "cycles through a list"
3988 def __init__(self, *sequence):
3989 changeattr.__init__(self)
3990 if not len(sequence):
3991 sequence = self.defaultsequence
3992 self.sequence = sequence
3994 def attr(self, index):
3995 return self.sequence[index % len(self.sequence)]
3998 class changelinestyle(changesequence):
3999 defaultsequence = (canvas.linestyle.solid,
4000 canvas.linestyle.dashed,
4001 canvas.linestyle.dotted,
4002 canvas.linestyle.dashdotted)
4005 class changestrokedfilled(changesequence):
4006 defaultsequence = (canvas.stroked(), canvas.filled())
4009 class changefilledstroked(changesequence):
4010 defaultsequence = (canvas.filled(), canvas.stroked())
4014 ################################################################################
4015 # styles
4016 ################################################################################
4019 class symbol:
4021 def cross(self, x, y):
4022 return (path._moveto(x-0.5*self._size, y-0.5*self._size),
4023 path._lineto(x+0.5*self._size, y+0.5*self._size),
4024 path._moveto(x-0.5*self._size, y+0.5*self._size),
4025 path._lineto(x+0.5*self._size, y-0.5*self._size))
4027 def plus(self, x, y):
4028 return (path._moveto(x-0.707106781*self._size, y),
4029 path._lineto(x+0.707106781*self._size, y),
4030 path._moveto(x, y-0.707106781*self._size),
4031 path._lineto(x, y+0.707106781*self._size))
4033 def square(self, x, y):
4034 return (path._moveto(x-0.5*self._size, y-0.5 * self._size),
4035 path._lineto(x+0.5*self._size, y-0.5 * self._size),
4036 path._lineto(x+0.5*self._size, y+0.5 * self._size),
4037 path._lineto(x-0.5*self._size, y+0.5 * self._size),
4038 path.closepath())
4040 def triangle(self, x, y):
4041 return (path._moveto(x-0.759835685*self._size, y-0.438691337*self._size),
4042 path._lineto(x+0.759835685*self._size, y-0.438691337*self._size),
4043 path._lineto(x, y+0.877382675*self._size),
4044 path.closepath())
4046 def circle(self, x, y):
4047 return (path._arc(x, y, 0.564189583*self._size, 0, 360),
4048 path.closepath())
4050 def diamond(self, x, y):
4051 return (path._moveto(x-0.537284965*self._size, y),
4052 path._lineto(x, y-0.930604859*self._size),
4053 path._lineto(x+0.537284965*self._size, y),
4054 path._lineto(x, y+0.930604859*self._size),
4055 path.closepath())
4057 def __init__(self, symbol=helper.nodefault,
4058 size="0.2 cm", symbolattrs=canvas.stroked(),
4059 errorscale=0.5, errorbarattrs=(),
4060 lineattrs=None):
4061 self.size_str = size
4062 if symbol is helper.nodefault:
4063 self._symbol = changesymbol.cross()
4064 else:
4065 self._symbol = symbol
4066 self._symbolattrs = symbolattrs
4067 self.errorscale = errorscale
4068 self._errorbarattrs = errorbarattrs
4069 self._lineattrs = lineattrs
4071 def iteratedict(self):
4072 result = {}
4073 result["symbol"] = _iterateattr(self._symbol)
4074 result["size"] = _iterateattr(self.size_str)
4075 result["symbolattrs"] = _iterateattrs(self._symbolattrs)
4076 result["errorscale"] = _iterateattr(self.errorscale)
4077 result["errorbarattrs"] = _iterateattrs(self._errorbarattrs)
4078 result["lineattrs"] = _iterateattrs(self._lineattrs)
4079 return result
4081 def iterate(self):
4082 return symbol(**self.iteratedict())
4084 def othercolumnkey(self, key, index):
4085 raise ValueError("unsuitable key '%s'" % key)
4087 def setcolumns(self, graph, columns):
4088 def checkpattern(key, index, pattern, iskey, isindex):
4089 if key is not None:
4090 match = pattern.match(key)
4091 if match:
4092 if isindex is not None: raise ValueError("multiple key specification")
4093 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4094 key = None
4095 iskey = match.groups()[0]
4096 isindex = index
4097 return key, iskey, isindex
4099 self.xi = self.xmini = self.xmaxi = None
4100 self.dxi = self.dxmini = self.dxmaxi = None
4101 self.yi = self.ymini = self.ymaxi = None
4102 self.dyi = self.dymini = self.dymaxi = None
4103 self.xkey = self.ykey = None
4104 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4105 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4106 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4107 XMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
4108 YMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
4109 XMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
4110 YMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
4111 DXPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4112 DYPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4113 DXMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
4114 DYMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
4115 DXMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
4116 DYMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
4117 for key, index in columns.items():
4118 key, self.xkey, self.xi = checkpattern(key, index, XPattern, self.xkey, self.xi)
4119 key, self.ykey, self.yi = checkpattern(key, index, YPattern, self.ykey, self.yi)
4120 key, self.xkey, self.xmini = checkpattern(key, index, XMinPattern, self.xkey, self.xmini)
4121 key, self.ykey, self.ymini = checkpattern(key, index, YMinPattern, self.ykey, self.ymini)
4122 key, self.xkey, self.xmaxi = checkpattern(key, index, XMaxPattern, self.xkey, self.xmaxi)
4123 key, self.ykey, self.ymaxi = checkpattern(key, index, YMaxPattern, self.ykey, self.ymaxi)
4124 key, self.xkey, self.dxi = checkpattern(key, index, DXPattern, self.xkey, self.dxi)
4125 key, self.ykey, self.dyi = checkpattern(key, index, DYPattern, self.ykey, self.dyi)
4126 key, self.xkey, self.dxmini = checkpattern(key, index, DXMinPattern, self.xkey, self.dxmini)
4127 key, self.ykey, self.dymini = checkpattern(key, index, DYMinPattern, self.ykey, self.dymini)
4128 key, self.xkey, self.dxmaxi = checkpattern(key, index, DXMaxPattern, self.xkey, self.dxmaxi)
4129 key, self.ykey, self.dymaxi = checkpattern(key, index, DYMaxPattern, self.ykey, self.dymaxi)
4130 if key is not None:
4131 self.othercolumnkey(key, index)
4132 if None in (self.xkey, self.ykey): raise ValueError("incomplete axis specification")
4133 if (len(filter(None, (self.xmini, self.dxmini, self.dxi))) > 1 or
4134 len(filter(None, (self.ymini, self.dymini, self.dyi))) > 1 or
4135 len(filter(None, (self.xmaxi, self.dxmaxi, self.dxi))) > 1 or
4136 len(filter(None, (self.ymaxi, self.dymaxi, self.dyi))) > 1):
4137 raise ValueError("multiple errorbar definition")
4138 if ((self.xi is None and self.dxi is not None) or
4139 (self.yi is None and self.dyi is not None) or
4140 (self.xi is None and self.dxmini is not None) or
4141 (self.yi is None and self.dymini is not None) or
4142 (self.xi is None and self.dxmaxi is not None) or
4143 (self.yi is None and self.dymaxi is not None)):
4144 raise ValueError("errorbar definition start value missing")
4145 self.xaxis = graph.axes[self.xkey]
4146 self.yaxis = graph.axes[self.ykey]
4148 def minmidmax(self, point, i, mini, maxi, di, dmini, dmaxi):
4149 min = max = mid = None
4150 try:
4151 mid = point[i] + 0.0
4152 except (TypeError, ValueError):
4153 pass
4154 try:
4155 if di is not None: min = point[i] - point[di]
4156 elif dmini is not None: min = point[i] - point[dmini]
4157 elif mini is not None: min = point[mini] + 0.0
4158 except (TypeError, ValueError):
4159 pass
4160 try:
4161 if di is not None: max = point[i] + point[di]
4162 elif dmaxi is not None: max = point[i] + point[dmaxi]
4163 elif maxi is not None: max = point[maxi] + 0.0
4164 except (TypeError, ValueError):
4165 pass
4166 if mid is not None:
4167 if min is not None and min > mid: raise ValueError("minimum error in errorbar")
4168 if max is not None and max < mid: raise ValueError("maximum error in errorbar")
4169 else:
4170 if min is not None and max is not None and min > max: raise ValueError("minimum/maximum error in errorbar")
4171 return min, mid, max
4173 def keyrange(self, points, i, mini, maxi, di, dmini, dmaxi):
4174 allmin = allmax = None
4175 if filter(None, (mini, maxi, di, dmini, dmaxi)) is not None:
4176 for point in points:
4177 min, mid, max = self.minmidmax(point, i, mini, maxi, di, dmini, dmaxi)
4178 if min is not None and (allmin is None or min < allmin): allmin = min
4179 if mid is not None and (allmin is None or mid < allmin): allmin = mid
4180 if mid is not None and (allmax is None or mid > allmax): allmax = mid
4181 if max is not None and (allmax is None or max > allmax): allmax = max
4182 else:
4183 for point in points:
4184 try:
4185 value = point[i] + 0.0
4186 if allmin is None or point[i] < allmin: allmin = point[i]
4187 if allmax is None or point[i] > allmax: allmax = point[i]
4188 except (TypeError, ValueError):
4189 pass
4190 return allmin, allmax
4192 def getranges(self, points):
4193 xmin, xmax = self.keyrange(points, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4194 ymin, ymax = self.keyrange(points, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4195 return {self.xkey: (xmin, xmax), self.ykey: (ymin, ymax)}
4197 def _drawerrorbar(self, graph, topleft, top, topright,
4198 left, center, right,
4199 bottomleft, bottom, bottomright, point=None):
4200 if left is not None:
4201 if right is not None:
4202 left1 = graph._addpos(*(left+(0, -self._errorsize)))
4203 left2 = graph._addpos(*(left+(0, self._errorsize)))
4204 right1 = graph._addpos(*(right+(0, -self._errorsize)))
4205 right2 = graph._addpos(*(right+(0, self._errorsize)))
4206 graph.stroke(path.path(path._moveto(*left1),
4207 graph._connect(*(left1+left2)),
4208 path._moveto(*left),
4209 graph._connect(*(left+right)),
4210 path._moveto(*right1),
4211 graph._connect(*(right1+right2))),
4212 *self.errorbarattrs)
4213 elif center is not None:
4214 left1 = graph._addpos(*(left+(0, -self._errorsize)))
4215 left2 = graph._addpos(*(left+(0, self._errorsize)))
4216 graph.stroke(path.path(path._moveto(*left1),
4217 graph._connect(*(left1+left2)),
4218 path._moveto(*left),
4219 graph._connect(*(left+center))),
4220 *self.errorbarattrs)
4221 else:
4222 left1 = graph._addpos(*(left+(0, -self._errorsize)))
4223 left2 = graph._addpos(*(left+(0, self._errorsize)))
4224 left3 = graph._addpos(*(left+(self._errorsize, 0)))
4225 graph.stroke(path.path(path._moveto(*left1),
4226 graph._connect(*(left1+left2)),
4227 path._moveto(*left),
4228 graph._connect(*(left+left3))),
4229 *self.errorbarattrs)
4230 if right is not None and left is None:
4231 if center is not None:
4232 right1 = graph._addpos(*(right+(0, -self._errorsize)))
4233 right2 = graph._addpos(*(right+(0, self._errorsize)))
4234 graph.stroke(path.path(path._moveto(*right1),
4235 graph._connect(*(right1+right2)),
4236 path._moveto(*right),
4237 graph._connect(*(right+center))),
4238 *self.errorbarattrs)
4239 else:
4240 right1 = graph._addpos(*(right+(0, -self._errorsize)))
4241 right2 = graph._addpos(*(right+(0, self._errorsize)))
4242 right3 = graph._addpos(*(right+(-self._errorsize, 0)))
4243 graph.stroke(path.path(path._moveto(*right1),
4244 graph._connect(*(right1+right2)),
4245 path._moveto(*right),
4246 graph._connect(*(right+right3))),
4247 *self.errorbarattrs)
4249 if bottom is not None:
4250 if top is not None:
4251 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
4252 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
4253 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
4254 top2 = graph._addpos(*(top+(self._errorsize, 0)))
4255 graph.stroke(path.path(path._moveto(*bottom1),
4256 graph._connect(*(bottom1+bottom2)),
4257 path._moveto(*bottom),
4258 graph._connect(*(bottom+top)),
4259 path._moveto(*top1),
4260 graph._connect(*(top1+top2))),
4261 *self.errorbarattrs)
4262 elif center is not None:
4263 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
4264 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
4265 graph.stroke(path.path(path._moveto(*bottom1),
4266 graph._connect(*(bottom1+bottom2)),
4267 path._moveto(*bottom),
4268 graph._connect(*(bottom+center))),
4269 *self.errorbarattrs)
4270 else:
4271 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
4272 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
4273 bottom3 = graph._addpos(*(bottom+(0, self._errorsize)))
4274 graph.stroke(path.path(path._moveto(*bottom1),
4275 graph._connect(*(bottom1+bottom2)),
4276 path._moveto(*bottom),
4277 graph._connect(*(bottom+bottom3))),
4278 *self.errorbarattrs)
4279 if top is not None and bottom is None:
4280 if center is not None:
4281 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
4282 top2 = graph._addpos(*(top+(self._errorsize, 0)))
4283 graph.stroke(path.path(path._moveto(*top1),
4284 graph._connect(*(top1+top2)),
4285 path._moveto(*top),
4286 graph._connect(*(top+center))),
4287 *self.errorbarattrs)
4288 else:
4289 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
4290 top2 = graph._addpos(*(top+(self._errorsize, 0)))
4291 top3 = graph._addpos(*(top+(0, -self._errorsize)))
4292 graph.stroke(path.path(path._moveto(*top1),
4293 graph._connect(*(top1+top2)),
4294 path._moveto(*top),
4295 graph._connect(*(top+top3))),
4296 *self.errorbarattrs)
4297 if bottomleft is not None:
4298 if topleft is not None and bottomright is None:
4299 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
4300 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
4301 graph.stroke(path.path(path._moveto(*bottomleft1),
4302 graph._connect(*(bottomleft1+bottomleft)),
4303 graph._connect(*(bottomleft+topleft)),
4304 graph._connect(*(topleft+topleft1))),
4305 *self.errorbarattrs)
4306 elif bottomright is not None and topleft is None:
4307 bottomleft1 = graph._addpos(*(bottomleft+(0, self._errorsize)))
4308 bottomright1 = graph._addpos(*(bottomright+(0, self._errorsize)))
4309 graph.stroke(path.path(path._moveto(*bottomleft1),
4310 graph._connect(*(bottomleft1+bottomleft)),
4311 graph._connect(*(bottomleft+bottomright)),
4312 graph._connect(*(bottomright+bottomright1))),
4313 *self.errorbarattrs)
4314 elif bottomright is None and topleft is None:
4315 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
4316 bottomleft2 = graph._addpos(*(bottomleft+(0, self._errorsize)))
4317 graph.stroke(path.path(path._moveto(*bottomleft1),
4318 graph._connect(*(bottomleft1+bottomleft)),
4319 graph._connect(*(bottomleft+bottomleft2))),
4320 *self.errorbarattrs)
4321 if topright is not None:
4322 if bottomright is not None and topleft is None:
4323 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
4324 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
4325 graph.stroke(path.path(path._moveto(*topright1),
4326 graph._connect(*(topright1+topright)),
4327 graph._connect(*(topright+bottomright)),
4328 graph._connect(*(bottomright+bottomright1))),
4329 *self.errorbarattrs)
4330 elif topleft is not None and bottomright is None:
4331 topright1 = graph._addpos(*(topright+(0, -self._errorsize)))
4332 topleft1 = graph._addpos(*(topleft+(0, -self._errorsize)))
4333 graph.stroke(path.path(path._moveto(*topright1),
4334 graph._connect(*(topright1+topright)),
4335 graph._connect(*(topright+topleft)),
4336 graph._connect(*(topleft+topleft1))),
4337 *self.errorbarattrs)
4338 elif topleft is None and bottomright is None:
4339 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
4340 topright2 = graph._addpos(*(topright+(0, -self._errorsize)))
4341 graph.stroke(path.path(path._moveto(*topright1),
4342 graph._connect(*(topright1+topright)),
4343 graph._connect(*(topright+topright2))),
4344 *self.errorbarattrs)
4345 if bottomright is not None and bottomleft is None and topright is None:
4346 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
4347 bottomright2 = graph._addpos(*(bottomright+(0, self._errorsize)))
4348 graph.stroke(path.path(path._moveto(*bottomright1),
4349 graph._connect(*(bottomright1+bottomright)),
4350 graph._connect(*(bottomright+bottomright2))),
4351 *self.errorbarattrs)
4352 if topleft is not None and bottomleft is None and topright is None:
4353 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
4354 topleft2 = graph._addpos(*(topleft+(0, -self._errorsize)))
4355 graph.stroke(path.path(path._moveto(*topleft1),
4356 graph._connect(*(topleft1+topleft)),
4357 graph._connect(*(topleft+topleft2))),
4358 *self.errorbarattrs)
4359 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4360 graph.stroke(path.path(path._moveto(*bottomleft),
4361 graph._connect(*(bottomleft+bottomright)),
4362 graph._connect(*(bottomright+topright)),
4363 graph._connect(*(topright+topleft)),
4364 path.closepath()),
4365 *self.errorbarattrs)
4367 def _drawsymbol(self, canvas, x, y, point=None):
4368 canvas.draw(path.path(*self.symbol(self, x, y)), *self.symbolattrs)
4370 def drawsymbol(self, canvas, x, y, point=None):
4371 self._drawsymbol(canvas, unit.topt(x), unit.topt(y), point)
4373 def key(self, c, x, y, width, height):
4374 if self._symbolattrs is not None:
4375 self._drawsymbol(c, x + 0.5 * width, y + 0.5 * height)
4376 if self._lineattrs is not None:
4377 c.stroke(path._line(x, y + 0.5 * height, x + width, y + 0.5 * height), *self.lineattrs)
4379 def drawpoints(self, graph, points):
4380 xaxismin, xaxismax = self.xaxis.getdatarange()
4381 yaxismin, yaxismax = self.yaxis.getdatarange()
4382 self.size = unit.length(_getattr(self.size_str), default_type="v")
4383 self._size = unit.topt(self.size)
4384 self.symbol = _getattr(self._symbol)
4385 self.symbolattrs = _getattrs(helper.ensuresequence(self._symbolattrs))
4386 self.errorbarattrs = _getattrs(helper.ensuresequence(self._errorbarattrs))
4387 self._errorsize = self.errorscale * self._size
4388 self.errorsize = self.errorscale * self.size
4389 self.lineattrs = _getattrs(helper.ensuresequence(self._lineattrs))
4390 if self._lineattrs is not None:
4391 clipcanvas = graph.clipcanvas()
4392 lineels = []
4393 haserror = filter(None, (self.xmini, self.ymini, self.xmaxi, self.ymaxi,
4394 self.dxi, self.dyi, self.dxmini, self.dymini, self.dxmaxi, self.dymaxi)) is not None
4395 moveto = 1
4396 for point in points:
4397 drawsymbol = 1
4398 xmin, x, xmax = self.minmidmax(point, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4399 ymin, y, ymax = self.minmidmax(point, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4400 if x is not None and x < xaxismin: drawsymbol = 0
4401 elif x is not None and x > xaxismax: drawsymbol = 0
4402 elif y is not None and y < yaxismin: drawsymbol = 0
4403 elif y is not None and y > yaxismax: drawsymbol = 0
4404 # elif haserror: # TODO: correct clipcanvas handling
4405 # if xmin is not None and xmin < xaxismin: drawsymbol = 0
4406 # elif xmax is not None and xmax < xaxismin: drawsymbol = 0
4407 # elif xmax is not None and xmax > xaxismax: drawsymbol = 0
4408 # elif xmin is not None and xmin > xaxismax: drawsymbol = 0
4409 # elif ymin is not None and ymin < yaxismin: drawsymbol = 0
4410 # elif ymax is not None and ymax < yaxismin: drawsymbol = 0
4411 # elif ymax is not None and ymax > yaxismax: drawsymbol = 0
4412 # elif ymin is not None and ymin > yaxismax: drawsymbol = 0
4413 xpos=ypos=topleft=top=topright=left=center=right=bottomleft=bottom=bottomright=None
4414 if x is not None and y is not None:
4415 try:
4416 center = xpos, ypos = graph._pos(x, y, xaxis=self.xaxis, yaxis=self.yaxis)
4417 except (ValueError, OverflowError): # XXX: exceptions???
4418 pass
4419 if haserror:
4420 if y is not None:
4421 if xmin is not None: left = graph._pos(xmin, y, xaxis=self.xaxis, yaxis=self.yaxis)
4422 if xmax is not None: right = graph._pos(xmax, y, xaxis=self.xaxis, yaxis=self.yaxis)
4423 if x is not None:
4424 if ymax is not None: top = graph._pos(x, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4425 if ymin is not None: bottom = graph._pos(x, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4426 if x is None or y is None:
4427 if ymax is not None:
4428 if xmin is not None: topleft = graph._pos(xmin, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4429 if xmax is not None: topright = graph._pos(xmax, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4430 if ymin is not None:
4431 if xmin is not None: bottomleft = graph._pos(xmin, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4432 if xmax is not None: bottomright = graph._pos(xmax, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4433 if drawsymbol:
4434 if self._errorbarattrs is not None and haserror:
4435 self._drawerrorbar(graph, topleft, top, topright,
4436 left, center, right,
4437 bottomleft, bottom, bottomright, point)
4438 if self._symbolattrs is not None and xpos is not None and ypos is not None:
4439 self._drawsymbol(graph, xpos, ypos, point)
4440 if xpos is not None and ypos is not None:
4441 if moveto:
4442 lineels.append(path._moveto(xpos, ypos))
4443 moveto = 0
4444 else:
4445 lineels.append(path._lineto(xpos, ypos))
4446 else:
4447 moveto = 1
4448 self.path = path.path(*lineels)
4449 if self._lineattrs is not None:
4450 clipcanvas.stroke(self.path, *self.lineattrs)
4453 class changesymbol(changesequence): pass
4456 class _changesymbolcross(changesymbol):
4457 defaultsequence = (symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond)
4460 class _changesymbolplus(changesymbol):
4461 defaultsequence = (symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross)
4464 class _changesymbolsquare(changesymbol):
4465 defaultsequence = (symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus)
4468 class _changesymboltriangle(changesymbol):
4469 defaultsequence = (symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square)
4472 class _changesymbolcircle(changesymbol):
4473 defaultsequence = (symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle)
4476 class _changesymboldiamond(changesymbol):
4477 defaultsequence = (symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle)
4480 class _changesymbolsquaretwice(changesymbol):
4481 defaultsequence = (symbol.square, symbol.square, symbol.triangle, symbol.triangle,
4482 symbol.circle, symbol.circle, symbol.diamond, symbol.diamond)
4485 class _changesymboltriangletwice(changesymbol):
4486 defaultsequence = (symbol.triangle, symbol.triangle, symbol.circle, symbol.circle,
4487 symbol.diamond, symbol.diamond, symbol.square, symbol.square)
4490 class _changesymbolcircletwice(changesymbol):
4491 defaultsequence = (symbol.circle, symbol.circle, symbol.diamond, symbol.diamond,
4492 symbol.square, symbol.square, symbol.triangle, symbol.triangle)
4495 class _changesymboldiamondtwice(changesymbol):
4496 defaultsequence = (symbol.diamond, symbol.diamond, symbol.square, symbol.square,
4497 symbol.triangle, symbol.triangle, symbol.circle, symbol.circle)
4500 changesymbol.cross = _changesymbolcross
4501 changesymbol.plus = _changesymbolplus
4502 changesymbol.square = _changesymbolsquare
4503 changesymbol.triangle = _changesymboltriangle
4504 changesymbol.circle = _changesymbolcircle
4505 changesymbol.diamond = _changesymboldiamond
4506 changesymbol.squaretwice = _changesymbolsquaretwice
4507 changesymbol.triangletwice = _changesymboltriangletwice
4508 changesymbol.circletwice = _changesymbolcircletwice
4509 changesymbol.diamondtwice = _changesymboldiamondtwice
4512 class line(symbol):
4514 def __init__(self, lineattrs=helper.nodefault):
4515 if lineattrs is helper.nodefault:
4516 lineattrs = (changelinestyle(), canvas.linejoin.round)
4517 symbol.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
4520 class rect(symbol):
4522 def __init__(self, palette=color.palette.Gray):
4523 self.palette = palette
4524 self.colorindex = None
4525 symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
4527 def iterate(self):
4528 raise RuntimeError("style is not iterateable")
4530 def othercolumnkey(self, key, index):
4531 if key == "color":
4532 self.colorindex = index
4533 else:
4534 symbol.othercolumnkey(self, key, index)
4536 def _drawerrorbar(self, graph, topleft, top, topright,
4537 left, center, right,
4538 bottomleft, bottom, bottomright, point=None):
4539 color = point[self.colorindex]
4540 if color is not None:
4541 if color != self.lastcolor:
4542 self.rectclipcanvas.set(self.palette.getcolor(color))
4543 if bottom is not None and left is not None:
4544 bottomleft = left[0], bottom[1]
4545 if bottom is not None and right is not None:
4546 bottomright = right[0], bottom[1]
4547 if top is not None and right is not None:
4548 topright = right[0], top[1]
4549 if top is not None and left is not None:
4550 topleft = left[0], top[1]
4551 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4552 self.rectclipcanvas.fill(path.path(path._moveto(*bottomleft),
4553 graph._connect(*(bottomleft+bottomright)),
4554 graph._connect(*(bottomright+topright)),
4555 graph._connect(*(topright+topleft)),
4556 path.closepath()))
4558 def drawpoints(self, graph, points):
4559 if self.colorindex is None:
4560 raise RuntimeError("column 'color' not set")
4561 self.lastcolor = None
4562 self.rectclipcanvas = graph.clipcanvas()
4563 symbol.drawpoints(self, graph, points)
4565 def key(self, c, x, y, width, height):
4566 raise RuntimeError("style doesn't yet provide a key")
4569 class text(symbol):
4571 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=textmodule.halign.center, **args):
4572 self.textindex = None
4573 self.textdx_str = textdx
4574 self.textdy_str = textdy
4575 self._textattrs = textattrs
4576 symbol.__init__(self, **args)
4578 def iteratedict(self):
4579 result = symbol.iteratedict()
4580 result["textattrs"] = _iterateattr(self._textattrs)
4581 return result
4583 def iterate(self):
4584 return textsymbol(**self.iteratedict())
4586 def othercolumnkey(self, key, index):
4587 if key == "text":
4588 self.textindex = index
4589 else:
4590 symbol.othercolumnkey(self, key, index)
4592 def _drawsymbol(self, graph, x, y, point=None):
4593 symbol._drawsymbol(self, graph, x, y, point)
4594 if None not in (x, y, point[self.textindex]) and self._textattrs is not None:
4595 graph._text(x + self._textdx, y + self._textdy, str(point[self.textindex]), *helper.ensuresequence(self.textattrs))
4597 def drawpoints(self, graph, points):
4598 self.textdx = unit.length(_getattr(self.textdx_str), default_type="v")
4599 self.textdy = unit.length(_getattr(self.textdy_str), default_type="v")
4600 self._textdx = unit.topt(self.textdx)
4601 self._textdy = unit.topt(self.textdy)
4602 if self._textattrs is not None:
4603 self.textattrs = _getattr(self._textattrs)
4604 if self.textindex is None:
4605 raise RuntimeError("column 'text' not set")
4606 symbol.drawpoints(self, graph, points)
4608 def key(self, c, x, y, width, height):
4609 raise RuntimeError("style doesn't yet provide a key")
4612 class arrow(symbol):
4614 def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
4615 self.linelength_str = linelength
4616 self.arrowsize_str = arrowsize
4617 self.arrowattrs = arrowattrs
4618 self.arrowdict = arrowdict
4619 self.epsilon = epsilon
4620 self.sizeindex = self.angleindex = None
4621 symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
4623 def iterate(self):
4624 raise RuntimeError("style is not iterateable")
4626 def othercolumnkey(self, key, index):
4627 if key == "size":
4628 self.sizeindex = index
4629 elif key == "angle":
4630 self.angleindex = index
4631 else:
4632 symbol.othercolumnkey(self, key, index)
4634 def _drawsymbol(self, graph, x, y, point=None):
4635 if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
4636 if point[self.sizeindex] > self.epsilon:
4637 dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
4638 x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
4639 y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
4640 x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
4641 y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
4642 graph.stroke(path.line(x1, y1, x2, y2),
4643 canvas.earrow(self.arrowsize*point[self.sizeindex],
4644 **self.arrowdict),
4645 *helper.ensuresequence(self.arrowattrs))
4647 def drawpoints(self, graph, points):
4648 self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
4649 self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
4650 self._arrowsize = unit.topt(self.arrowsize)
4651 self._linelength = unit.topt(self.linelength)
4652 if self.sizeindex is None:
4653 raise RuntimeError("column 'size' not set")
4654 if self.angleindex is None:
4655 raise RuntimeError("column 'angle' not set")
4656 symbol.drawpoints(self, graph, points)
4658 def key(self, c, x, y, width, height):
4659 raise RuntimeError("style doesn't yet provide a key")
4662 class _bariterator(changeattr):
4664 def attr(self, index):
4665 return index, self.counter
4668 class bar:
4670 def __init__(self, fromzero=1, stacked=0, skipmissing=1, xbar=0,
4671 barattrs=helper.nodefault, _usebariterator=helper.nodefault, _previousbar=None):
4672 self.fromzero = fromzero
4673 self.stacked = stacked
4674 self.skipmissing = skipmissing
4675 self.xbar = xbar
4676 if barattrs is helper.nodefault:
4677 self._barattrs = (canvas.stroked(color.gray.black), changecolor.Rainbow())
4678 else:
4679 self._barattrs = barattrs
4680 if _usebariterator is helper.nodefault:
4681 self.bariterator = _bariterator()
4682 else:
4683 self.bariterator = _usebariterator
4684 self.previousbar = _previousbar
4686 def iteratedict(self):
4687 result = {}
4688 result["barattrs"] = _iterateattrs(self._barattrs)
4689 return result
4691 def iterate(self):
4692 return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
4693 _usebariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
4695 def setcolumns(self, graph, columns):
4696 def checkpattern(key, index, pattern, iskey, isindex):
4697 if key is not None:
4698 match = pattern.match(key)
4699 if match:
4700 if isindex is not None: raise ValueError("multiple key specification")
4701 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4702 key = None
4703 iskey = match.groups()[0]
4704 isindex = index
4705 return key, iskey, isindex
4707 xkey = ykey = None
4708 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4709 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4710 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4711 xi = yi = None
4712 for key, index in columns.items():
4713 key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
4714 key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
4715 if key is not None:
4716 self.othercolumnkey(key, index)
4717 if None in (xkey, ykey): raise ValueError("incomplete axis specification")
4718 if self.xbar:
4719 self.nkey, self.ni = ykey, yi
4720 self.vkey, self.vi = xkey, xi
4721 else:
4722 self.nkey, self.ni = xkey, xi
4723 self.vkey, self.vi = ykey, yi
4724 self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
4726 def getranges(self, points):
4727 index, count = _getattr(self.bariterator)
4728 if count != 1 and self.stacked != 1:
4729 if self.stacked > 1:
4730 index = divmod(index, self.stacked)[0]
4732 vmin = vmax = None
4733 for point in points:
4734 if not self.skipmissing:
4735 if count != 1 and self.stacked != 1:
4736 self.naxis.setname(point[self.ni], index)
4737 else:
4738 self.naxis.setname(point[self.ni])
4739 try:
4740 v = point[self.vi] + 0.0
4741 if vmin is None or v < vmin: vmin = v
4742 if vmax is None or v > vmax: vmax = v
4743 except (TypeError, ValueError):
4744 pass
4745 else:
4746 if self.skipmissing:
4747 if count != 1 and self.stacked != 1:
4748 self.naxis.setname(point[self.ni], index)
4749 else:
4750 self.naxis.setname(point[self.ni])
4751 if self.fromzero:
4752 if vmin > 0: vmin = 0
4753 if vmax < 0: vmax = 0
4754 return {self.vkey: (vmin, vmax)}
4756 def drawpoints(self, graph, points):
4757 index, count = _getattr(self.bariterator)
4758 dostacked = (self.stacked != 0 and
4759 (self.stacked == 1 or divmod(index, self.stacked)[1]) and
4760 (self.stacked != 1 or index))
4761 if self.stacked > 1:
4762 index = divmod(index, self.stacked)[0]
4763 vmin, vmax = self.vaxis.getdatarange()
4764 self.barattrs = _getattrs(helper.ensuresequence(self._barattrs))
4765 if self.stacked:
4766 self.stackedvalue = {}
4767 for point in points:
4768 try:
4769 n = point[self.ni]
4770 v = point[self.vi]
4771 if self.stacked:
4772 self.stackedvalue[n] = v
4773 if count != 1 and self.stacked != 1:
4774 minid = (n, index, 0)
4775 maxid = (n, index, 1)
4776 else:
4777 minid = (n, 0)
4778 maxid = (n, 1)
4779 if self.xbar:
4780 x1pos, y1pos = graph._pos(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
4781 x2pos, y2pos = graph._pos(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4782 else:
4783 x1pos, y1pos = graph._pos(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
4784 x2pos, y2pos = graph._pos(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
4785 if dostacked:
4786 if self.xbar:
4787 x3pos, y3pos = graph._pos(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
4788 x4pos, y4pos = graph._pos(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
4789 else:
4790 x3pos, y3pos = graph._pos(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4791 x4pos, y4pos = graph._pos(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4792 else:
4793 if self.fromzero:
4794 if self.xbar:
4795 x3pos, y3pos = graph._pos(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4796 x4pos, y4pos = graph._pos(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
4797 else:
4798 x3pos, y3pos = graph._pos(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4799 x4pos, y4pos = graph._pos(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4800 else:
4801 x3pos, y3pos = graph._tickpoint(maxid, axis=self.naxis)
4802 x4pos, y4pos = graph._tickpoint(minid, axis=self.naxis)
4803 if self.barattrs is not None:
4804 graph.fill(path.path(path._moveto(x1pos, y1pos),
4805 graph._connect(x1pos, y1pos, x2pos, y2pos),
4806 graph._connect(x2pos, y2pos, x3pos, y3pos),
4807 graph._connect(x3pos, y3pos, x4pos, y4pos),
4808 graph._connect(x4pos, y4pos, x1pos, y1pos), # no closepath (might not be straight)
4809 path.closepath()), *self.barattrs)
4810 except (TypeError, ValueError): pass
4812 def key(self, c, x, y, width, height):
4813 c.fill(path._rect(x, y, width, height), *self.barattrs)
4816 #class surface:
4818 # def setcolumns(self, graph, columns):
4819 # self.columns = columns
4821 # def getranges(self, points):
4822 # return {"x": (0, 10), "y": (0, 10), "z": (0, 1)}
4824 # def drawpoints(self, graph, points):
4825 # pass
4829 ################################################################################
4830 # data
4831 ################################################################################
4834 class data:
4836 defaultstyle = symbol
4838 def __init__(self, file, title=helper.nodefault, context={}, **columns):
4839 self.title = title
4840 if helper.isstring(file):
4841 self.data = datamodule.datafile(file)
4842 else:
4843 self.data = file
4844 if title is helper.nodefault:
4845 self.title = "(unknown)"
4846 else:
4847 self.title = title
4848 self.columns = {}
4849 for key, column in columns.items():
4850 try:
4851 self.columns[key] = self.data.getcolumnno(column)
4852 except datamodule.ColumnError:
4853 self.columns[key] = len(self.data.titles)
4854 self.data.addcolumn(column, context=context)
4856 def setstyle(self, graph, style):
4857 self.style = style
4858 self.style.setcolumns(graph, self.columns)
4860 def getranges(self):
4861 return self.style.getranges(self.data.data)
4863 def setranges(self, ranges):
4864 pass
4866 def draw(self, graph):
4867 self.style.drawpoints(graph, self.data.data)
4870 class function:
4872 defaultstyle = line
4874 def __init__(self, expression, title=helper.nodefault, min=None, max=None, points=100, parser=mathtree.parser(), context={}):
4875 if title is helper.nodefault:
4876 self.title = expression
4877 else:
4878 self.title = title
4879 self.min = min
4880 self.max = max
4881 self.points = points
4882 self.context = context
4883 self.result, expression = [x.strip() for x in expression.split("=")]
4884 self.mathtree = parser.parse(expression)
4885 self.variable = None
4886 self.evalranges = 0
4888 def setstyle(self, graph, style):
4889 for variable in self.mathtree.VarList():
4890 if variable in graph.axes.keys():
4891 if self.variable is None:
4892 self.variable = variable
4893 else:
4894 raise ValueError("multiple variables found")
4895 if self.variable is None:
4896 raise ValueError("no variable found")
4897 self.xaxis = graph.axes[self.variable]
4898 self.style = style
4899 self.style.setcolumns(graph, {self.variable: 0, self.result: 1})
4901 def getranges(self):
4902 if self.evalranges:
4903 return self.style.getranges(self.data)
4904 if None not in (self.min, self.max):
4905 return {self.variable: (self.min, self.max)}
4907 def setranges(self, ranges):
4908 if ranges.has_key(self.variable):
4909 min, max = ranges[self.variable]
4910 if self.min is not None: min = self.min
4911 if self.max is not None: max = self.max
4912 vmin = self.xaxis.convert(min)
4913 vmax = self.xaxis.convert(max)
4914 self.data = []
4915 for i in range(self.points):
4916 self.context[self.variable] = x = self.xaxis.invert(vmin + (vmax-vmin)*i / (self.points-1.0))
4917 try:
4918 y = self.mathtree.Calc(**self.context)
4919 except (ArithmeticError, ValueError):
4920 y = None
4921 self.data.append((x, y))
4922 self.evalranges = 1
4924 def draw(self, graph):
4925 self.style.drawpoints(graph, self.data)
4928 class paramfunction:
4930 defaultstyle = line
4932 def __init__(self, varname, min, max, expression, title=helper.nodefault, points=100, parser=mathtree.parser(), context={}):
4933 if title is helper.nodefault:
4934 self.title = expression
4935 else:
4936 self.title = title
4937 self.varname = varname
4938 self.min = min
4939 self.max = max
4940 self.points = points
4941 self.expression = {}
4942 self.mathtrees = {}
4943 varlist, expressionlist = expression.split("=")
4944 parsestr = mathtree.ParseStr(expressionlist)
4945 for key in varlist.split(","):
4946 key = key.strip()
4947 if self.mathtrees.has_key(key):
4948 raise ValueError("multiple assignment in tuple")
4949 try:
4950 self.mathtrees[key] = parser.ParseMathTree(parsestr)
4951 break
4952 except mathtree.CommaFoundMathTreeParseError, e:
4953 self.mathtrees[key] = e.MathTree
4954 else:
4955 raise ValueError("unpack tuple of wrong size")
4956 if len(varlist.split(",")) != len(self.mathtrees.keys()):
4957 raise ValueError("unpack tuple of wrong size")
4958 self.data = []
4959 for i in range(self.points):
4960 context[self.varname] = self.min + (self.max-self.min)*i / (self.points-1.0)
4961 line = []
4962 for key, tree in self.mathtrees.items():
4963 line.append(tree.Calc(**context))
4964 self.data.append(line)
4966 def setstyle(self, graph, style):
4967 self.style = style
4968 columns = {}
4969 for key, index in zip(self.mathtrees.keys(), xrange(sys.maxint)):
4970 columns[key] = index
4971 self.style.setcolumns(graph, columns)
4973 def getranges(self):
4974 return self.style.getranges(self.data)
4976 def setranges(self, ranges):
4977 pass
4979 def draw(self, graph):
4980 self.style.drawpoints(graph, self.data)