make all parts of the manual compile again; parts of the manual are still out of...
[PyX/mjg.git] / pyx / graph.py
blob25ea9d004092d4403ab03a60a90a91525bd2f52f
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 import re, math, string, sys
26 import bbox, box, canvas, color, deco, helper, path, style, unit, mathtree
27 import text as textmodule
28 import data as datamodule
29 import trafo as trafomodule
32 goldenmean = 0.5 * (math.sqrt(5) + 1)
35 ################################################################################
36 # maps
37 ################################################################################
39 class _Imap:
40 """interface definition of a map
41 maps convert a value into another value by bijective transformation f"""
43 def convert(self, x):
44 "returns f(x)"
46 def invert(self, y):
47 "returns f^-1(y) where f^-1 is the inverse transformation (x=f^-1(f(x)) for all x)"
49 def setbasepoints(self, basepoints):
50 """set basepoints for the convertions
51 basepoints are tuples (x, y) with y == f(x) and x == f^-1(y)
52 the number of basepoints needed might depend on the transformation
53 usually two pairs are needed like for linear maps, logarithmic maps, etc."""
56 class _linmap:
57 "linear mapping"
59 __implements__ = _Imap
61 def setbasepoints(self, basepoints):
62 self.dydx = (basepoints[1][1] - basepoints[0][1]) / float(basepoints[1][0] - basepoints[0][0])
63 self.dxdy = (basepoints[1][0] - basepoints[0][0]) / float(basepoints[1][1] - basepoints[0][1])
64 self.x1 = basepoints[0][0]
65 self.y1 = basepoints[0][1]
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 !!! TODO!!!
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 returned by an
1328 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 handled by the axispainters painter method; you may
1332 apprehend this as a size information comparable to a
1333 bounding box, which must be handled manually
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, *args, **kwargs):
1344 """initializes the instance
1345 - sets extent to zero
1346 - sets labels to an empty list"""
1347 canvas._canvas.__init__(self, *args, **kwargs)
1348 self.extent = 0
1349 self.labels = []
1352 class rotatetext:
1353 """create rotations accordingly to tick directions
1354 - upsidedown rotations are suppressed by rotating them by another 180 degree"""
1356 # __implements__ = sole implementation
1358 def __init__(self, direction, epsilon=1e-10):
1359 """initializes the instance
1360 - direction is an angle to be used relative to the tick direction
1361 - epsilon is the value by which 90 degrees can be exceeded before
1362 an 180 degree rotation is added"""
1363 self.direction = direction
1364 self.epsilon = epsilon
1366 def trafo(self, dx, dy):
1367 """returns a rotation transformation accordingly to the tick direction
1368 - dx and dy are the direction of the tick"""
1369 direction = self.direction + math.atan2(dy, dx) * 180 / math.pi
1370 while (direction > 90 + self.epsilon):
1371 direction -= 180
1372 while (direction < -90 - self.epsilon):
1373 direction += 180
1374 return trafomodule.rotate(direction)
1377 rotatetext.parallel = rotatetext(-90)
1378 rotatetext.orthogonal = rotatetext(0)
1381 class _Iaxispainter:
1382 "class for painting axes"
1384 def paint(self, axispos, axis, ac=None):
1385 """paint the axis into an axiscanvas
1386 - returns the axiscanvas
1387 - when no axiscanvas is provided (the typical case), a new
1388 axiscanvas is created. however, when extending an painter
1389 by inheritance, painting on the same axiscanvas is supported
1390 by setting the axiscanvas attribute
1391 - axispos is an instance, which implements _Iaxispos to
1392 define the tick positions
1393 - the axis and should not be modified (we may
1394 add some temporary variables like axis.ticks[i].temp_xxx,
1395 which might be used just temporary) -- the idea is that
1396 all things can be used several times
1397 - also do not modify the instance (self) -- even this
1398 instance might be used several times; thus do not modify
1399 attributes like self.titleattrs etc. (use local copies)
1400 - the method might access some additional attributes from
1401 the axis, e.g. the axis title -- the axis painter should
1402 document this behavior and rely on the availability of
1403 those attributes -> it becomes a question of the proper
1404 usage of the combination of axis & axispainter
1405 - the axiscanvas is a axiscanvas instance and should be
1406 filled with ticks, labels, title, etc.; note that the
1407 extent and labels instance variables should be handled
1408 as documented in the axiscanvas"""
1411 class _Iaxispos:
1412 """interface definition of axis tick position methods
1413 - these methods are used for the postitioning of the ticks
1414 when painting an axis"""
1415 # TODO: should we add a local transformation (for label text etc?)
1416 # (this might replace tickdirection (and even tickposition?))
1418 def baseline(self, x1=None, x2=None):
1419 """return the baseline as a path
1420 - x1 is the start position; if not set, the baseline starts
1421 from the beginning of the axis, which might imply a
1422 value outside of the graph coordinate range [0; 1]
1423 - x2 is analogous to x1, but for the end position"""
1425 def vbaseline(self, v1=None, v2=None):
1426 """return the baseline as a path
1427 - like baseline, but for graph coordinates"""
1429 def gridline(self, x):
1430 """return the gridline as a path for a given position x
1431 - might return None when no gridline is available"""
1433 def vgridline(self, v):
1434 """return the gridline as a path for a given position v
1435 in graph coordinates
1436 - might return None when no gridline is available"""
1438 def _tickpoint(self, x):
1439 """return the position at the baseline as a tuple (x, y) in
1440 postscript points for the position x"""
1442 def tickpoint(self, x):
1443 """return the position at the baseline as a tuple (x, y) in
1444 in PyX length for the position x"""
1446 def _vtickpoint(self, v):
1447 "like _tickpoint, but for graph coordinates"
1449 def vtickpoint(self, v):
1450 "like tickpoint, but for graph coordinates"
1452 def tickdirection(self, x):
1453 """return the direction of a tick as a tuple (dx, dy) for the
1454 position x"""
1456 def vtickdirection(self, v):
1457 """like tickposition, but for graph coordinates"""
1460 class _axispos:
1461 """implements those parts of _Iaxispos which can be build
1462 out of the axis convert method and other _Iaxispos methods
1463 (its designed to be inherited)"""
1465 def __init__(self, a):
1466 """initializes the instance
1467 - only the convert method is needed from the axis a"""
1468 self.axis = a
1470 def baseline(self, x1=None, x2=None):
1471 if x1 is None:
1472 if x2 is None:
1473 return self.vbaseline()
1474 else:
1475 return self.vbaseline(v2=self.axis.convert(x2))
1476 else:
1477 if x2 is None:
1478 return self.vbaseline(v1=self.axis.convert(x1))
1479 else:
1480 return self.vbaseline(v1=self.axis.convert(x1), v2=self.axis.convert(x2))
1482 def gridline(self, x):
1483 return self.vgridline(self.axis.convert(x))
1485 def _tickpoint(self, x):
1486 return self._vtickpoint(self.axis.convert(x))
1488 def tickpoint(self, x):
1489 return self.vtickpoint(self.axis.convert(x))
1491 def _vtickpoint(self, v):
1492 return map(unit.topt, self.vtickpoint(v))
1494 def tickdirection(self, x):
1495 return self.vtickdirection(self.axis.convert(x))
1498 class pathaxispos(_axispos):
1499 """axis tick position methods along an arbitrary path"""
1501 __implements__ = _Iaxispos
1503 def __init__(self, p, a, direction=1):
1504 self.path = p
1505 self.normpath = path.normpath(p)
1506 self.arclength = self.normpath.arclength(p)
1507 _axispos.__init__(self, a)
1508 self.direction = direction
1510 def vbaseline(self, v1=None, v2=None):
1511 if v1 is None:
1512 if v2 is None:
1513 return self.path
1514 else:
1515 return self.normpath.split(self.normpath.lentopar(v2 * self.arclength))[0]
1516 else:
1517 if v2 is None:
1518 return self.normpath.split(self.normpath.lentopar(v1 * self.arclength))[1]
1519 else:
1520 return self.normpath.split(*self.normpath.lentopar([v1 * self.arclength, v2 * self.arclength]))[1]
1522 def vgridline(self, v):
1523 return None
1525 def vtickpoint(self, v):
1526 return self.normpath.at(self.normpath.lentopar(v * self.arclength))
1528 def vtickdirection(self, v):
1529 t = self.normpath.tangent(self.normpath.lentopar(v * self.arclength))
1530 # XXX: path._begin and path._end missing!
1531 tbegin = map(unit.topt, t.begin())
1532 tend = map(unit.topt, t.end())
1533 dx = tend[0]-tbegin[0]
1534 dy = tend[1]-tbegin[1]
1535 norm = math.sqrt(dx*dx + dy*dy)
1536 if self.direction == 1:
1537 return dy/norm, -dx/norm
1538 elif self.direction == -1:
1539 return -dy/norm, dx/norm
1540 raise RuntimeError("unknown direction")
1543 class axistitlepainter:
1544 """class for painting an axis title
1545 - the axis must have a title attribute when using this painter;
1546 this title might be None"""
1548 __implements__ = _Iaxispainter
1550 def __init__(self, titledist="0.3 cm",
1551 titleattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1552 titledirection=rotatetext.parallel,
1553 titlepos=0.5,
1554 texrunner=textmodule.defaulttexrunner):
1555 """initialized the instance
1556 - titledist is a visual PyX length giving the distance
1557 of the title from the axis extent already there (a title might
1558 be added after labels or other things are plotted already)
1559 - labelattrs is a list of attributes for a texrunners text
1560 method; a single is allowed without being a list; None
1561 turns off the title
1562 - titledirection is an instance of rotatetext or None
1563 - titlepos is the position of the title in graph coordinates
1564 - texrunner is the texrunner to be used to create text
1565 (the texrunner is available for further use in derived
1566 classes as instance variable texrunner)"""
1567 self.titledist_str = titledist
1568 self.titleattrs = titleattrs
1569 self.titledirection = titledirection
1570 self.titlepos = titlepos
1571 self.texrunner = texrunner
1573 def paint(self, axispos, axis, ac=None):
1574 if ac is None:
1575 ac = axiscanvas()
1576 if axis.title is not None and self.titleattrs is not None:
1577 titledist = unit.length(self.titledist_str, default_type="v")
1578 x, y = axispos._vtickpoint(self.titlepos)
1579 dx, dy = axispos.vtickdirection(self.titlepos)
1580 titleattrs = helper.ensurelist(self.titleattrs)
1581 if self.titledirection is not None:
1582 titleattrs.append(self.titledirection.trafo(dx, dy))
1583 title = self.texrunner._text(x, y, axis.title, *titleattrs)
1584 ac.extent += titledist
1585 title.linealign(ac.extent, dx, dy)
1586 ac.extent += title.extent(dx, dy)
1587 ac.insert(title)
1588 return ac
1591 class axispainter(axistitlepainter):
1592 """class for painting the ticks and labels of an axis
1593 - the inherited titleaxispainter is used to paint the title of
1594 the axis
1595 - note that the type of the elements of ticks given as an argument
1596 of the paint method must be suitable for the tick position methods
1597 of the axis"""
1599 __implements__ = _Iaxispainter
1601 defaultticklengths = ["%0.5f cm" % (0.2*goldenmean**(-i)) for i in range(10)]
1603 def __init__(self, innerticklengths=defaultticklengths,
1604 outerticklengths=None,
1605 tickattrs=(),
1606 gridattrs=None,
1607 zerolineattrs=(),
1608 baselineattrs=style.linecap.square,
1609 labeldist="0.3 cm",
1610 labelattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1611 labeldirection=None,
1612 labelhequalize=0,
1613 labelvequalize=1,
1614 **kwargs):
1615 """initializes the instance
1616 - innerticklenths and outerticklengths are two lists of
1617 visual PyX lengths for ticks, subticks, etc. plotted inside
1618 and outside of the graph; when a single value is given, it
1619 is used for all tick levels; None turns off ticks inside or
1620 outside of the graph
1621 - tickattrs are a list of stroke attributes for the ticks;
1622 a single entry is allowed without being a list; None turns
1623 off ticks
1624 - gridlineattrs are a list of lists used as stroke
1625 attributes for ticks, subticks etc.; when a single list
1626 is given, it is used for ticks, subticks, etc.; a single
1627 entry is allowed without being a list; None turns off
1628 gridlines
1629 - zerolineattrs are a list of stroke attributes for a grid
1630 line at axis value zero; a single entry is allowed without
1631 being a list; None turns off the zeroline
1632 - baselineattrs are a list of stroke attributes for a grid
1633 line at axis value zero; a single entry is allowed without
1634 being a list; None turns off the baseline
1635 - labeldist is a visual PyX length for the distance of the labels
1636 from the axis baseline
1637 - labelattrs is a list of attributes for a texrunners text
1638 method; a single entry is allowed without being a list;
1639 None turns off the labels
1640 - titledirection is an instance of rotatetext or None
1641 - labelhequalize and labelvequalize (booleans) perform an equal
1642 alignment for straight vertical and horizontal axes, respectively
1643 - futher keyword arguments are passed to axistitlepainter"""
1644 # TODO: access to axis.divisor -- document, remove, ... ???
1645 self.innerticklengths_str = innerticklengths
1646 self.outerticklengths_str = outerticklengths
1647 self.tickattrs = tickattrs
1648 self.gridattrs = gridattrs
1649 self.zerolineattrs = zerolineattrs
1650 self.baselineattrs = baselineattrs
1651 self.labeldist_str = labeldist
1652 self.labelattrs = labelattrs
1653 self.labeldirection = labeldirection
1654 self.labelhequalize = labelhequalize
1655 self.labelvequalize = labelvequalize
1656 axistitlepainter.__init__(self, **kwargs)
1658 def paint(self, axispos, axis, ac=None):
1659 if ac is None:
1660 ac = axiscanvas()
1661 else:
1662 raise RuntimeError("XXX") # XXX debug only
1663 labeldist = unit.length(self.labeldist_str, default_type="v")
1664 for tick in axis.ticks:
1665 if axis.divisor is not None: # XXX workaround for timeaxis
1666 tick.temp_v = axis.convert(float(tick) * axis.divisor)
1667 else:
1668 tick.temp_v = axis.convert(tick)
1669 tick.temp_x, tick.temp_y = axispos._vtickpoint(tick.temp_v)
1670 tick.temp_dx, tick.temp_dy = axispos.vtickdirection(tick.temp_v)
1672 # create & align tick.temp_labelbox
1673 for tick in axis.ticks:
1674 if tick.labellevel is not None:
1675 labelattrs = helper.getsequenceno(self.labelattrs, tick.labellevel)
1676 if labelattrs is not None:
1677 labelattrs = helper.ensurelist(labelattrs)[:]
1678 if self.labeldirection is not None:
1679 labelattrs.append(self.labeldirection.trafo(tick.temp_dx, tick.temp_dy))
1680 if tick.labelattrs is not None:
1681 labelattrs.extend(helper.ensurelist(tick.labelattrs))
1682 tick.temp_labelbox = self.texrunner._text(tick.temp_x, tick.temp_y, tick.label, *labelattrs)
1683 if len(axis.ticks) > 1:
1684 equaldirection = 1
1685 for tick in axis.ticks[1:]:
1686 if tick.temp_dx != axis.ticks[0].temp_dx or tick.temp_dy != axis.ticks[0].temp_dy:
1687 equaldirection = 0
1688 else:
1689 equaldirection = 0
1690 if equaldirection and ((not axis.ticks[0].temp_dx and self.labelvequalize) or
1691 (not axis.ticks[0].temp_dy and self.labelhequalize)):
1692 if self.labelattrs is not None:
1693 box.linealignequal([tick.temp_labelbox for tick in axis.ticks if tick.labellevel is not None],
1694 labeldist, axis.ticks[0].temp_dx, axis.ticks[0].temp_dy)
1695 else:
1696 for tick in axis.ticks:
1697 if tick.labellevel is not None and self.labelattrs is not None:
1698 tick.temp_labelbox.linealign(labeldist, tick.temp_dx, tick.temp_dy)
1700 def mkv(arg):
1701 if helper.issequence(arg):
1702 return [unit.length(a, default_type="v") for a in arg]
1703 if arg is not None:
1704 return unit.length(arg, default_type="v")
1705 innerticklengths = mkv(self.innerticklengths_str)
1706 outerticklengths = mkv(self.outerticklengths_str)
1708 for tick in axis.ticks:
1709 if tick.ticklevel is not None:
1710 innerticklength = helper.getitemno(innerticklengths, tick.ticklevel)
1711 outerticklength = helper.getitemno(outerticklengths, tick.ticklevel)
1712 if innerticklength is not None or outerticklength is not None:
1713 if innerticklength is None:
1714 innerticklength = 0
1715 if outerticklength is None:
1716 outerticklength = 0
1717 tickattrs = helper.getsequenceno(self.tickattrs, tick.ticklevel)
1718 if tickattrs is not None:
1719 _innerticklength = unit.topt(innerticklength)
1720 _outerticklength = unit.topt(outerticklength)
1721 x1 = tick.temp_x - tick.temp_dx * _innerticklength
1722 y1 = tick.temp_y - tick.temp_dy * _innerticklength
1723 x2 = tick.temp_x + tick.temp_dx * _outerticklength
1724 y2 = tick.temp_y + tick.temp_dy * _outerticklength
1725 ac.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(tickattrs))
1726 if tick != frac((0, 1)) or self.zerolineattrs is None:
1727 gridattrs = helper.getsequenceno(self.gridattrs, tick.ticklevel)
1728 if gridattrs is not None:
1729 self.stroke(axispos.vgridline(tick.temp_v), *helper.ensuresequence(gridattrs))
1730 if outerticklength is not None and unit.topt(outerticklength) > unit.topt(ac.extent):
1731 ac.extent = outerticklength
1732 if outerticklength is not None and unit.topt(-innerticklength) > unit.topt(ac.extent):
1733 ac.extent = -innerticklength
1734 if tick.labellevel is not None and self.labelattrs is not None:
1735 ac.insert(tick.temp_labelbox)
1736 ac.labels.append(tick.temp_labelbox)
1737 extent = tick.temp_labelbox.extent(tick.temp_dx, tick.temp_dy) + labeldist
1738 if unit.topt(extent) > unit.topt(ac.extent):
1739 ac.extent = extent
1740 if self.baselineattrs is not None:
1741 ac.stroke(axispos.vbaseline(), *helper.ensuresequence(self.baselineattrs))
1742 if self.zerolineattrs is not None:
1743 if len(axis.ticks) and axis.ticks[0] * axis.ticks[-1] < frac((0, 1)):
1744 ac.stroke(axispos.gridline(0), *helper.ensuresequence(self.zerolineattrs))
1746 # for tick in axis.ticks:
1747 # del tick.temp_v # we've inserted those temporary variables ... and do not care any longer about them
1748 # del tick.temp_x
1749 # del tick.temp_y
1750 # del tick.temp_dx
1751 # del tick.temp_dy
1752 # if tick.labellevel is not None and self.labelattrs is not None:
1753 # del tick.temp_labelbox
1755 axistitlepainter.paint(self, axispos, axis, ac=ac)
1757 return ac
1760 class linkaxispainter(axispainter):
1761 """class for painting a linked axis
1762 - the inherited axispainter is used to paint the axis
1763 - modifies some constructor defaults"""
1765 __implements__ = _Iaxispainter
1767 def __init__(self, zerolineattrs=None,
1768 labelattrs=None,
1769 titleattrs=None,
1770 **kwargs):
1771 """initializes the instance
1772 - the zerolineattrs default is set to None thus skipping the zeroline
1773 - the labelattrs default is set to None thus skipping the labels
1774 - the titleattrs default is set to None thus skipping the title
1775 - all keyword arguments are passed to axispainter"""
1776 axispainter.__init__(self, zerolineattrs=zerolineattrs,
1777 labelattrs=labelattrs,
1778 titleattrs=titleattrs,
1779 **kwargs)
1782 class splitaxispainter(axistitlepainter):
1783 """class for painting a splitaxis
1784 - the inherited titleaxispainter is used to paint the title of
1785 the axis
1786 - the splitaxispainter access the subaxes attribute of the axis"""
1788 __implements__ = _Iaxispainter
1790 def __init__(self, breaklinesdist="0.05 cm",
1791 breaklineslength="0.5 cm",
1792 breaklinesangle=-60,
1793 breaklinesattrs=(),
1794 **args):
1795 """initializes the instance
1796 - breaklinesdist is a visual length of the distance between
1797 the two lines of the axis break
1798 - breaklineslength is a visual length of the length of the
1799 two lines of the axis break
1800 - breaklinesangle is the angle of the lines of the axis break
1801 - breaklinesattrs are a list of stroke attributes for the
1802 axis break lines; a single entry is allowed without being a
1803 list; None turns off the break lines
1804 - futher keyword arguments are passed to axistitlepainter"""
1805 self.breaklinesdist_str = breaklinesdist
1806 self.breaklineslength_str = breaklineslength
1807 self.breaklinesangle = breaklinesangle
1808 self.breaklinesattrs = breaklinesattrs
1809 axistitlepainter.__init__(self, **args)
1811 def paint(self, axispos, axis, ac=None):
1812 if ac is None:
1813 ac = axiscanvas()
1814 else:
1815 raise RuntimeError("XXX") # XXX debug only
1816 for subaxis in axis.subaxes:
1817 subac = subaxis.finish(axispos)
1818 ac.insert(subac)
1819 if unit.topt(ac.extent) < unit.topt(subac.extent):
1820 ac.extent = subac.extent
1821 if self.breaklinesattrs is not None:
1822 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1823 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1824 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1825 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1826 breaklinesextent = (0.5*self.breaklinesdist*math.fabs(self.cos) +
1827 0.5*self.breaklineslength*math.fabs(self.sin))
1828 if unit.topt(ac.extent) < unit.topt(breaklinesextent):
1829 ac.extent = breaklinesextent
1830 for subaxis1, subaxis2 in zip(axis.subaxes[:-1], axis.subaxes[1:]):
1831 # use a tangent of the baseline (this is independent of the tickdirection)
1832 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1833 p = path.normpath(axispos.vbaseline(v, None))
1834 breakline = p.tangent(0, self.breaklineslength)
1835 widthline = p.tangent(0, self.breaklinesdist).transformed(trafomodule.rotate(self.breaklinesangle+90, *breakline.begin()))
1836 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1837 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1838 breakline = breakline.transformed(trafomodule.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1839 breakline1 = breakline.transformed(trafomodule.translate(*towidth))
1840 breakline2 = breakline.transformed(trafomodule.translate(-towidth[0], -towidth[1]))
1841 ac.fill(path.path(path.moveto(*breakline1.begin()),
1842 path.lineto(*breakline1.end()),
1843 path.lineto(*breakline2.end()),
1844 path.lineto(*breakline2.begin()),
1845 path.closepath()), color.gray.white)
1846 ac.stroke(breakline1, *helper.ensuresequence(self.breaklinesattrs))
1847 ac.stroke(breakline2, *helper.ensuresequence(self.breaklinesattrs))
1848 axistitlepainter.paint(self, axispos, axis, ac=ac)
1849 return ac
1852 class linksplitaxispainter(splitaxispainter):
1853 """class for painting a linked splitaxis
1854 - the inherited splitaxispainter is used to paint the axis
1855 - modifies some constructor defaults"""
1857 __implements__ = _Iaxispainter
1859 def __init__(self, titleattrs=None, **kwargs):
1860 """initializes the instance
1861 - the titleattrs default is set to None thus skipping the title
1862 - all keyword arguments are passed to splitaxispainter"""
1863 splitaxispainter.__init__(self, titleattrs=titleattrs, **kwargs)
1866 class baraxispainter(axistitlepainter):
1867 """class for painting a baraxis
1868 - the inherited titleaxispainter is used to paint the title of
1869 the axis
1870 - the baraxispainter access the multisubaxis, subaxis names, texts, and
1871 relsizes attributes"""
1873 __implements__ = _Iaxispainter
1875 def __init__(self, innerticklength=None,
1876 outerticklength=None,
1877 tickattrs=(),
1878 baselineattrs=style.linecap.square,
1879 namedist="0.3 cm",
1880 nameattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1881 namedirection=None,
1882 namepos=0.5,
1883 namehequalize=0,
1884 namevequalize=1,
1885 **args):
1886 """initializes the instance
1887 - innerticklength and outerticklength are a visual length of
1888 the ticks to be plotted at the axis baseline to visually
1889 separate the bars; if neither innerticklength nor
1890 outerticklength are set, not ticks are plotted
1891 - breaklinesattrs are a list of stroke attributes for the
1892 axis tick; a single entry is allowed without being a
1893 list; None turns off the ticks
1894 - namedist is a visual PyX length for the distance of the bar
1895 names from the axis baseline
1896 - nameattrs is a list of attributes for a texrunners text
1897 method; a single entry is allowed without being a list;
1898 None turns off the names
1899 - namedirection is an instance of rotatetext or None
1900 - namehequalize and namevequalize (booleans) perform an equal
1901 alignment for straight vertical and horizontal axes, respectively
1902 - futher keyword arguments are passed to axistitlepainter"""
1903 self.innerticklength_str = innerticklength
1904 self.outerticklength_str = outerticklength
1905 self.tickattrs = tickattrs
1906 self.baselineattrs = baselineattrs
1907 self.namedist_str = namedist
1908 self.nameattrs = nameattrs
1909 self.namedirection = namedirection
1910 self.namepos = namepos
1911 self.namehequalize = namehequalize
1912 self.namevequalize = namevequalize
1913 axistitlepainter.__init__(self, **args)
1915 def paint(self, axispos, axis, ac=None):
1916 if ac is None:
1917 ac = axiscanvas()
1918 else:
1919 raise RuntimeError("XXX") # XXX debug only
1920 if axis.multisubaxis is not None:
1921 for subaxis in axis.subaxis:
1922 subac = subaxis.finish(axis, self.texrunner)
1923 ac.insert(subac)
1924 if unit.topt(ac.extent) < unit.topt(subac.extent):
1925 ac.extent = subac.extent
1926 namepos = []
1927 for name in axis.names:
1928 v = axis.convert((name, self.namepos))
1929 x, y = axispos._vtickpoint(v)
1930 dx, dy = axispos.vtickdirection(v)
1931 namepos.append((v, x, y, dx, dy))
1932 nameboxes = []
1933 if self.nameattrs is not None:
1934 for (v, x, y, dx, dy), name in zip(namepos, axis.names):
1935 nameattrs = helper.ensurelist(self.nameattrs)[:]
1936 if self.namedirection is not None:
1937 nameattrs.append(self.namedirection.trafo(tick.temp_dx, tick.temp_dy))
1938 if axis.texts.has_key(name):
1939 nameboxes.append(self.texrunner._text(x, y, str(axis.texts[name]), *nameattrs))
1940 elif axis.texts.has_key(str(name)):
1941 nameboxes.append(self.texrunner._text(x, y, str(axis.texts[str(name)]), *nameattrs))
1942 else:
1943 nameboxes.append(self.texrunner._text(x, y, str(name), *nameattrs))
1944 labeldist = ac.extent + unit.length(self.namedist_str, default_type="v")
1945 if len(namepos) > 1:
1946 equaldirection = 1
1947 for np in namepos[1:]:
1948 if np[3] != namepos[0][3] or np[4] != namepos[0][4]:
1949 equaldirection = 0
1950 else:
1951 equaldirection = 0
1952 if equaldirection and ((not namepos[0][3] and self.namevequalize) or
1953 (not namepos[0][4] and self.namehequalize)):
1954 box.linealignequal(nameboxes, labeldist, namepos[0][3], namepos[0][4])
1955 else:
1956 for namebox, np in zip(nameboxes, namepos):
1957 namebox.linealign(labeldist, np[3], np[4])
1958 if self.innerticklength_str is not None:
1959 innerticklength = unit.length(self.innerticklength_str, default_type="v")
1960 _innerticklength = unit.topt(innerticklength)
1961 if self.tickattrs is not None and unit.topt(ac.extent) < -_innerticklength:
1962 ac.extent = -innerticklength
1963 elif self.outerticklength_str is not None:
1964 innerticklength = _innerticklength = 0
1965 if self.outerticklength_str is not None:
1966 outerticklength = unit.length(self.outerticklength_str, default_type="v")
1967 _outerticklength = unit.topt(outerticklength)
1968 if self.tickattrs is not None and unit.topt(ac.extent) < _outerticklength:
1969 ac.extent = outerticklength
1970 elif self.innerticklength_str is not None:
1971 outerticklength = _outerticklength = 0
1972 for (v, x, y, dx, dy), namebox in zip(namepos, nameboxes):
1973 newextent = namebox.extent(dx, dy) + labeldist
1974 if unit.topt(ac.extent) < unit.topt(newextent):
1975 ac.extent = newextent
1976 if self.tickattrs is not None and (self.innerticklength_str is not None or self.outerticklength_str is not None):
1977 for pos in axis.relsizes:
1978 if pos == axis.relsizes[0]:
1979 pos -= axis.firstdist
1980 elif pos != axis.relsizes[-1]:
1981 pos -= 0.5 * axis.dist
1982 v = pos / axis.relsizes[-1]
1983 x, y = axispos._vtickpoint(v)
1984 dx, dy = axispos.vtickdirection(v)
1985 x1 = x - dx * _innerticklength
1986 y1 = y - dy * _innerticklength
1987 x2 = x + dx * _outerticklength
1988 y2 = y + dy * _outerticklength
1989 ac.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(self.tickattrs))
1990 if self.baselineattrs is not None:
1991 p = axispos.vbaseline()
1992 if p is not None:
1993 ac.stroke(p, *helper.ensuresequence(self.baselineattrs))
1994 for namebox in nameboxes:
1995 ac.insert(namebox)
1996 axistitlepainter.paint(self, axispos, axis, ac=ac)
1999 class linkbaraxispainter(baraxispainter):
2000 """class for painting a linked baraxis
2001 - the inherited baraxispainter is used to paint the axis
2002 - modifies some constructor defaults"""
2004 __implements__ = _Iaxispainter
2006 def __init__(self, nameattrs=None, titleattrs=None, **kwargs):
2007 """initializes the instance
2008 - the titleattrs default is set to None thus skipping the title
2009 - the nameattrs default is set to None thus skipping the names
2010 - all keyword arguments are passed to axispainter"""
2011 baraxispainter.__init__(self, nameattrs=nameattrs, titleattrs=titleattrs, **kwargs)
2014 ################################################################################
2015 # axes
2016 ################################################################################
2019 class _Iaxis:
2020 """interface definition of a axis
2021 - an axis should implement an convert and invert method like
2022 _Imap, but this is not part of this interface definition;
2023 one possibility is to mix-in a proper map class, but special
2024 purpose axes might do something else"""
2026 def convert(self, x):
2027 "convert a value into graph coordinates"
2029 def invert(self, v):
2030 "invert a graph coordinate to a axis value"
2032 def getrelsize(self):
2033 """returns the relative size (width) of the axis
2034 - for use in splitaxis, baraxis etc.
2035 - might return None if no size is available"""
2037 def setrange(self, min=None, max=None):
2038 """set the axis data range
2039 - the type of min and max must fit to the axis
2040 - min<max; the axis might be reversed, but this is
2041 expressed internally only (min<max all the time)
2042 - the axis might not apply the change of the range
2043 (e.g. when the axis range is fixed by the user),
2044 but usually the range is extended to contain the
2045 given range
2046 - for invalid parameters (e.g. negativ values at an
2047 logarithmic axis), an exception should be raised
2048 - a RuntimeError is raised, when setrange is called
2049 after the finish method"""
2051 def getrange(self):
2052 """return data range as a tuple (min, max)
2053 - min<max; the axis might be reversed, but this is
2054 expressed internally only
2055 - a RuntimeError exception is raised when no
2056 range is available"""
2058 def finish(self, axispos):
2059 """finishes the axis
2060 - axispos implements _Iaxispos
2061 - the finish method returns an axiscanvas, which should be
2062 insertable into the graph to finally paint the axis
2063 - any modification of the axis range should be disabled after
2064 the finish method was called"""
2065 # TODO: be more specific about exceptions
2067 def createlinkaxis(self, **kwargs):
2068 """create a link axis to the axis itself
2069 - typically, a link axis is a axis, which share almost
2070 all properties with the axis it is linked to
2071 - typically, the painter gets replaced by a painter
2072 which doesn't put any text to the axis"""
2075 class _axis:
2076 """base implementation a regular axis
2077 - typical usage is to mix-in a linmap or a logmap to
2078 complete the definition"""
2080 def __init__(self, min=None, max=None, reverse=0, divisor=1,
2081 title=None, painter=axispainter(), texter=defaulttexter(),
2082 density=1, maxworse=2):
2083 """initializes the instance
2084 - min and max fix the axis minimum and maximum, respectively;
2085 they are determined by the data to be plotted, when not fixed
2086 - reverse (boolean) reverses the minimum and the maximum of
2087 the axis
2088 - numerical divisor for the axis partitioning
2089 - title is a string containing the axis title
2090 - axispainter is the axis painter (should implement _Ipainter)
2091 - texter is the texter (should implement _Itexter)
2092 - density is a global parameter for the axis paritioning and
2093 axis rating; its default is 1, but the range 0.5 to 2.5 should
2094 be usefull to get less or more ticks by the automatic axis
2095 partitioning
2096 - maxworse is a number of trials with worse tick rating
2097 before giving up (usually it should not be needed to increase
2098 this value; increasing the number will slow down the automatic
2099 axis partitioning considerably)
2100 - note that some methods of this class want to access a
2101 part and a rating attribute of the instance; those
2102 attributes should be initialized by the constructors
2103 of derived classes"""
2104 if None not in (min, max) and min > max:
2105 min, max, reverse = max, min, not reverse
2106 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
2107 self.divisor = divisor
2108 self.title = title
2109 self.painter = painter
2110 self.texter = texter
2111 self.density = density
2112 self.maxworse = maxworse
2113 self.canconvert = 0
2114 self.finished = 0
2115 self._setinternalrange()
2117 def _setinternalrange(self, min=None, max=None):
2118 if not self.fixmin and min is not None and (self.min is None or min < self.min):
2119 self.min = min
2120 if not self.fixmax and max is not None and (self.max is None or max > self.max):
2121 self.max = max
2122 if None not in (self.min, self.max):
2123 self.canconvert = 1
2124 if not self.reverse:
2125 self.setbasepoints(((self.min, 1), (self.max, 0)))
2126 else:
2127 self.setbasepoints(((self.min, 0), (self.max, 1)))
2129 def _getinternalrange(self):
2130 return self.min, self.max
2132 def _forceinternalrange(self, range):
2133 self.min, self.max = range
2134 self._setinternalrange()
2136 def setrange(self, min=None, max=None):
2137 if self.finished:
2138 raise RuntimeError("axis was already finished")
2139 self._setinternalrange(min, max)
2141 def getrange(self):
2142 if self.min is not None and self.max is not None:
2143 return self.min, self.max
2145 def checkfraclist(self, fracs):
2146 "orders a list of fracs, equal entries are not allowed"
2147 if not len(fracs): return []
2148 sorted = list(fracs)
2149 sorted.sort()
2150 last = sorted[0]
2151 for item in sorted[1:]:
2152 if last == item:
2153 raise ValueError("duplicate entry found")
2154 last = item
2155 return sorted
2157 def finish(self, axispos, texrunner):
2158 if self.finished:
2159 return
2161 min, max = self.getrange()
2162 parter = parterpos = None
2163 if self.part is not None:
2164 self.part = helper.ensurelist(self.part)
2165 for p, i in zip(self.part, xrange(sys.maxint)):
2166 if hasattr(p, "defaultpart"):
2167 if parter is not None:
2168 raise RuntimeError("only one partitioner allowed")
2169 parter = p
2170 parterpos = i
2171 if parter is None:
2172 self.ticks = self.checkfraclist(self.part)
2173 else:
2174 self.part[:parterpos] = self.checkfraclist(self.part[:parterpos])
2175 self.part[parterpos+1:] = self.checkfraclist(self.part[parterpos+1:])
2176 # if self.divisor is not None: # XXX workaround for timeaxis
2177 self.ticks = _mergeticklists(
2178 _mergeticklists(self.part[:parterpos],
2179 parter.defaultpart(min/self.divisor,
2180 max/self.divisor,
2181 not self.fixmin,
2182 not self.fixmax)),
2183 self.part[parterpos+1:])
2184 # else:
2185 # self.ticks = _mergeticklists(
2186 # _mergeticklists(self.part[:parterpos],
2187 # parter.defaultpart(min,
2188 # max,
2189 # not self.fixmin,
2190 # not self.fixmax)),
2191 # self.part[parterpos+1:])
2192 else:
2193 self.ticks = []
2194 # lesspart and morepart can be called after defaultpart;
2195 # this works although some axes may share their autoparting,
2196 # because the axes are processed sequentially
2197 first = 1
2198 worse = 0
2199 while worse < self.maxworse:
2200 if parter is not None:
2201 newticks = parter.lesspart()
2202 if parterpos is not None and newticks is not None:
2203 newticks = _mergeticklists(_mergeticklists(self.part[:parterpos], newticks), self.part[parterpos+1:])
2204 else:
2205 newticks = None
2206 if newticks is not None:
2207 if first:
2208 bestrate = self.rater.ratepart(self, self.ticks, self.density)
2209 variants = [[bestrate, self.ticks]]
2210 first = 0
2211 newrate = self.rater.ratepart(self, newticks, self.density)
2212 variants.append([newrate, newticks])
2213 if newrate < bestrate:
2214 bestrate = newrate
2215 worse = 0
2216 else:
2217 worse += 1
2218 else:
2219 worse += 1
2220 worse = 0
2221 while worse < self.maxworse:
2222 if parter is not None:
2223 newticks = parter.morepart()
2224 if parterpos is not None and newticks is not None:
2225 newticks = _mergeticklists(_mergeticklists(self.part[:parterpos], newticks), self.part[parterpos+1:])
2226 else:
2227 newticks = None
2228 if newticks is not None:
2229 if first:
2230 bestrate = self.rater.ratepart(self, self.ticks, self.density)
2231 variants = [[bestrate, self.ticks]]
2232 first = 0
2233 newrate = self.rater.ratepart(self, newticks, self.density)
2234 variants.append([newrate, newticks])
2235 if newrate < bestrate:
2236 bestrate = newrate
2237 worse = 0
2238 else:
2239 worse += 1
2240 else:
2241 worse += 1
2243 if not first:
2244 variants.sort()
2245 if self.painter is not None:
2246 i = 0
2247 bestrate = None
2248 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
2249 saverange = self._getinternalrange()
2250 self.ticks = variants[i][1]
2251 if len(self.ticks):
2252 # if self.divisor is not None: # XXX workaround for timeaxis
2253 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2254 # else:
2255 # self.setrange(self.ticks[0], self.ticks[-1])
2256 self.texter.labels(self.ticks)
2257 ac = self.painter.paint(axispos, self)
2258 ratelayout = self.rater.ratelayout(ac, self.density)
2259 if ratelayout is not None:
2260 variants[i][0] += ratelayout
2261 variants[i].append(ac)
2262 else:
2263 variants[i][0] = None
2264 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
2265 bestrate = variants[i][0]
2266 self._forceinternalrange(saverange)
2267 i += 1
2268 if bestrate is None:
2269 raise RuntimeError("no valid axis partitioning found")
2270 variants = [variant for variant in variants[:i] if variant[0] is not None]
2271 variants.sort()
2272 self.ticks = variants[0][1]
2273 if len(self.ticks):
2274 # if self.divisor is not None: # XXX workaround for timeaxis
2275 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2276 # else:
2277 # self.setrange(self.ticks[0], self.ticks[-1])
2278 ac = variants[0][2]
2279 else:
2280 if len(self.ticks):
2281 # if self.divisor is not None: # XXX workaround for timeaxis
2282 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2283 # else:
2284 # self.setrange(self.ticks[0], self.ticks[-1])
2285 ac = axiscanvas()
2286 else:
2287 if len(self.ticks):
2288 # if self.divisor is not None: # XXX workaround for timeaxis
2289 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2290 # else:
2291 # self.setrange(self.ticks[0], self.ticks[-1])
2292 self.texter.labels(self.ticks)
2293 ac = self.painter.paint(axispos, self)
2294 self.finished = 1
2295 return ac
2297 def createlinkaxis(self, **args):
2298 return linkaxis(self, **args)
2301 class linaxis(_axis, _linmap):
2302 """implementation of a linear axis"""
2304 __implements__ = _Iaxis
2306 def __init__(self, part=autolinpart(), rater=axisrater(), **args):
2307 """initializes the instance
2308 - the part attribute contains a list of one partitioner
2309 (a partitioner implements _Ipart) or/and some (manually
2310 set) ticks (implementing _Itick); a single entry might
2311 be passed without wrapping it into a list; the partitioner
2312 and the tick instances must fit to the type of the axis
2313 (e.g. they should be valid parameters to the axis convert
2314 method); the ticks and the partitioner results are mixed
2315 by _mergeticklists
2316 - the rater implements _Irater and is used to rate different
2317 tick lists created by the partitioner (after merging with
2318 manully set ticks)
2319 - futher keyword arguments are passed to _axis"""
2320 _axis.__init__(self, **args)
2321 if self.fixmin and self.fixmax:
2322 self.relsize = self.max - self.min
2323 self.part = part
2324 self.rater = rater
2327 class logaxis(_axis, _logmap):
2328 """implementation of a logarithmic axis"""
2330 __implements__ = _Iaxis
2332 def __init__(self, part=autologpart(), rater=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
2333 """initializes the instance
2334 - the part attribute contains a list of one partitioner
2335 (a partitioner implements _Ipart) or/and some (manually
2336 set) ticks (implementing _Itick); a single entry might
2337 be passed without wrapping it into a list; the partitioner
2338 and the tick instances must fit to the type of the axis
2339 (e.g. they should be valid parameters to the axis convert
2340 method); the ticks and the partitioner results are mixed
2341 by _mergeticklists
2342 - the rater implements _Irater and is used to rate different
2343 tick lists created by the partitioner (after merging with
2344 manully set ticks)
2345 - futher keyword arguments are passed to _axis"""
2346 _axis.__init__(self, **args)
2347 if self.fixmin and self.fixmax:
2348 self.relsize = math.log(self.max) - math.log(self.min)
2349 self.part = part
2350 self.rater = rater
2353 class linkaxis:
2354 """a axis linked to an already existing regular axis
2355 - almost all properties of the axis are "copied" from the
2356 axis this axis is linked to
2357 - usually, linked axis are used to create an axis to an
2358 existing axis with different painting properties; linked
2359 axis can be used to plot an axis twice at the opposite
2360 sides of a graphxy or even to share an axis between
2361 different graphs!"""
2363 __implements__ = _Iaxis
2365 def __init__(self, linkedaxis, painter=linkaxispainter()):
2366 """initializes the instance
2367 - it gets a axis this linkaxis is linked to
2368 - it gets a painter to be used for this linked axis"""
2369 self.linkedaxis = linkedaxis
2370 self.painter = painter
2371 self.finished = 0
2373 def __getattr__(self, attr):
2374 """access to unkown attributes are handed over to the
2375 axis this linkaxis is linked to"""
2376 return getattr(self.linkedaxis, attr)
2378 def finish(self, axispos, texrunner):
2379 """finishes the axis
2380 - instead of performing the hole finish process
2381 (paritioning, rating, etc.) just a painter call
2382 is performed"""
2383 if self.finished:
2384 return
2385 self.finished = 1
2386 self.linkedaxis.finish(axispos, texrunner) # multiple calls to finish!
2387 return self.painter.paint(axispos, self)
2390 class _subaxispos:
2391 """implementation of the _Iaxispos interface for subaxis
2392 - provides the _Iaxispos interface for axis which contain
2393 several subaxes
2394 - typical usage by mix-in this class into an "main" axis and
2395 calling the painter(s) of the subaxes from the specialized
2396 painter of the "main" axis; the subaxes need to have the
2397 attributes "baseaxis" (reference to the "main" axis) and
2398 "baseaxispos" (reference to the instance implementing the
2399 _Iaxispos interface of the "main" axis)"""
2401 __implements__ = _Iaxispos
2403 def baseline(self, x1=None, x2=None, axis=None):
2404 if x1 is not None:
2405 v1 = axis.convert(x1)
2406 else:
2407 v1 = 0
2408 if x2 is not None:
2409 v2 = axis.convert(x2)
2410 else:
2411 v2 = 1
2412 return axis.baseaxispos.vbaseline(v1, v2, axis=axis.baseaxis)
2414 def vbaseline(self, v1=None, v2=None, axis=None):
2415 if v1 is None:
2416 left = axis.vmin
2417 else:
2418 left = axis.vmin+v1*(axis.vmax-axis.vmin)
2419 if v2 is None:
2420 right = axis.vmax
2421 else:
2422 right = axis.vmin+v2*(axis.vmax-axis.vmin)
2423 return axis.baseaxispos.vbaseline(left, right, axis=axis.baseaxis)
2425 def gridline(self, x, axis=None):
2426 return axis.baseaxispos.vgridline(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2428 def vgridline(self, v, axis=None):
2429 return axis.baseaxispos.vgridline(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2431 def _tickpoint(self, x, axis=None):
2432 return axis.baseaxispos._vtickpoint(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2434 def tickpoint(self, x, axis=None):
2435 return axis.baseaxispos.vtickpoint(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2437 def _vtickpoint(self, v, axis=None):
2438 return axis.baseaxispos._vtickpoint(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2440 def vtickpoint(self, v, axis=None):
2441 return axis.baseaxispos.vtickpoint(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2443 def tickdirection(self, x, axis=None):
2444 return axis.baseaxispos.vtickdirection(axis.vmin+axis.convert(x)*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2446 def vtickdirection(self, v, axis=None):
2447 return axis.baseaxispos.vtickdirection(axis.vmin+v*(axis.vmax-axis.vmin), axis=axis.baseaxis)
2450 class splitaxis(_subaxispos):
2451 """implementation of a split axis
2452 - a split axis contains several (sub-)axes with
2453 non-overlapping data ranges -- between these subaxes
2454 the axis is "splitted"
2455 - (just to get sure: a splitaxis can contain other
2456 splitaxes as its subaxes)
2457 - a splitaxis implements the _Iaxispos for its subaxes
2458 by inheritance from _subaxispos"""
2460 __implements__ = _Iaxis, _Iaxispos
2462 def __init__(self, subaxes, splitlist=0.5, splitdist=0.1, relsizesplitdist=1,
2463 title=None, painter=splitaxispainter()):
2464 """initializes the instance
2465 - subaxes is a list of subaxes
2466 - splitlist is a list of graph coordinates, where the splitting
2467 of the main axis should be performed; a single entry (splitting
2468 two axes) doesn't need to be wrapped into a list; if the list
2469 isn't long enough for the subaxes, missing entries are considered
2470 to be None;
2471 - splitdist is the size of the splitting in graph coordinates, when
2472 the associated splitlist entry is not None
2473 - relsizesplitdist: a None entry in splitlist means, that the
2474 position of the splitting should be calculated out of the
2475 relsize values of conrtibuting subaxes (the size of the
2476 splitting is relsizesplitdist in values of the relsize values
2477 of the axes)
2478 - title is the title of the axis as a string
2479 - painter is the painter of the axis; it should be specialized to
2480 the splitaxis
2481 - the relsize of the splitaxis is the sum of the relsizes of the
2482 subaxes including the relsizesplitdist"""
2483 self.subaxes = subaxes
2484 self.painter = painter
2485 self.title = title
2486 self.splitlist = helper.ensurelist(splitlist)
2487 for subaxis in self.subaxes:
2488 subaxis.vmin = None
2489 subaxis.vmax = None
2490 self.subaxes[0].vmin = 0
2491 self.subaxes[0].vminover = None
2492 self.subaxes[-1].vmax = 1
2493 self.subaxes[-1].vmaxover = None
2494 for i in xrange(len(self.splitlist)):
2495 if self.splitlist[i] is not None:
2496 self.subaxes[i].vmax = self.splitlist[i] - 0.5*splitdist
2497 self.subaxes[i].vmaxover = self.splitlist[i]
2498 self.subaxes[i+1].vmin = self.splitlist[i] + 0.5*splitdist
2499 self.subaxes[i+1].vminover = self.splitlist[i]
2500 i = 0
2501 while i < len(self.subaxes):
2502 if self.subaxes[i].vmax is None:
2503 j = relsize = relsize2 = 0
2504 while self.subaxes[i + j].vmax is None:
2505 relsize += self.subaxes[i + j].relsize + relsizesplitdist
2506 j += 1
2507 relsize += self.subaxes[i + j].relsize
2508 vleft = self.subaxes[i].vmin
2509 vright = self.subaxes[i + j].vmax
2510 for k in range(i, i + j):
2511 relsize2 += self.subaxes[k].relsize
2512 self.subaxes[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
2513 relsize2 += 0.5 * relsizesplitdist
2514 self.subaxes[k].vmaxover = self.subaxes[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
2515 relsize2 += 0.5 * relsizesplitdist
2516 self.subaxes[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
2517 if i == 0 and i + j + 1 == len(self.subaxes):
2518 self.relsize = relsize
2519 i += j + 1
2520 else:
2521 i += 1
2523 self.fixmin = self.subaxes[0].fixmin
2524 if self.fixmin:
2525 self.min = self.subaxes[0].min
2526 self.fixmax = self.subaxes[-1].fixmax
2527 if self.fixmax:
2528 self.max = self.subaxes[-1].max
2529 self.finished = 0
2531 def getrange(self):
2532 min = self.subaxes[0].getrange()
2533 max = self.subaxes[-1].getrange()
2534 try:
2535 return min[0], max[1]
2536 except TypeError:
2537 return None
2539 def setdatarange(self, min, max):
2540 self.subaxes[0].setdatarange(min, None)
2541 self.subaxes[-1].setdatarange(None, max)
2543 def gettickrange(self):
2544 min = self.subaxes[0].gettickrange()
2545 max = self.subaxes[-1].gettickrange()
2546 try:
2547 return min[0], max[1]
2548 except TypeError:
2549 return None
2551 def settickrange(self, min, max):
2552 self.subaxes[0].settickrange(min, None)
2553 self.subaxes[-1].settickrange(None, max)
2555 def convert(self, value):
2556 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2557 if value < self.subaxes[0].max:
2558 return self.subaxes[0].vmin + self.subaxes[0].convert(value)*(self.subaxes[0].vmax-self.subaxes[0].vmin)
2559 for axis in self.subaxes[1:-1]:
2560 if value > axis.min and value < axis.max:
2561 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
2562 if value > self.subaxes[-1].min:
2563 return self.subaxes[-1].vmin + self.subaxes[-1].convert(value)*(self.subaxes[-1].vmax-self.subaxes[-1].vmin)
2564 raise ValueError("value couldn't be assigned to a split region")
2566 def vbaseline(self, v1=None, v2=None, axis=None):
2567 if v1 is None:
2568 if axis.baseaxis.painter.breaklinesattrs is None: # XXX undocumented access to painter!?
2569 left = axis.vmin
2570 else:
2571 if axis.vminover is None:
2572 left = None
2573 else:
2574 left = axis.vminover
2575 else:
2576 left = axis.vmin+v1*(axis.vmax-axis.vmin)
2577 if v2 is None:
2578 if axis.baseaxis.painter.breaklinesattrs is None:
2579 right = axis.vmax
2580 else:
2581 if axis.vmaxover is None:
2582 right = None
2583 else:
2584 right = axis.vmaxover
2585 else:
2586 right = axis.vmin+v2*(axis.vmax-axis.vmin)
2587 return axis.baseaxispos.vbaseline(left, right, axis=axis.baseaxis)
2589 def finish(self, axispos, texrunner):
2590 if self.finished:
2591 return
2592 self.finished = 1
2593 for subaxis in self.subaxes:
2594 subaxis.baseaxispos = axispos
2595 subaxis.baseaxis = self
2596 return self.painter.paint(axispos, self)
2598 def createlinkaxis(self, **args):
2599 return linksplitaxis(self, **args)
2602 class linksplitaxis(linkaxis):
2603 """a splitaxis linked to an already existing splitaxis
2604 - inherits the access to a linked axis -- as before,
2605 basically only the painter is replaced
2606 - it must take care of the creation of linked axes of
2607 the subaxes"""
2609 __implements__ = _Iaxis
2611 def __init__(self, linkedaxis, painter=linksplitaxispainter(), subaxispainter=None):
2612 """initializes the instance
2613 - it gets a axis this linkaxis is linked to
2614 - it gets a painter to be used for this linked axis
2615 - it gets a list of painters to be used for the linkaxes
2616 of the subaxes; if None, the createlinkaxis of the subaxes
2617 are called without a painter parameter; if it is not a
2618 list, the subaxispainter is passed as the painter
2619 parameter to all createlinkaxis of the subaxes"""
2620 if subaxispainter is not None:
2621 if helper.issequence(subaxispainter):
2622 if len(linkedaxis.subaxes) != len(subaxispainter):
2623 raise RuntimeError("subaxes and subaxispainter lengths do not fit")
2624 self.subaxes = [a.createlinkaxis(painter=p) for a, p in zip(linkedaxis.subaxes, subaxispainter)]
2625 else:
2626 self.subaxes = [a.createlinkaxis(painter=subaxispainter) for a in linkedaxis.subaxes]
2627 else:
2628 self.subaxes = [a.createlinkaxis() for a in linkedaxis.subaxes]
2629 linkaxis.__init__(self, linkedaxis, painter=painter)
2631 def finish(self, axispos, texrunner):
2632 for subaxis in self.subaxes:
2633 subaxis.baseaxispos = axispos
2634 subaxis.baseaxis = self
2635 linkaxis.finish(self, axispos, texrunner)
2638 class baraxis(_subaxispos):
2639 """implementation of a axis for bar graphs
2640 - a bar axes is different from a splitaxis by the way it
2641 selects its subaxes: the convert method gets a list,
2642 where the first entry is a name selecting a subaxis out
2643 of a list; instead of the term "bar" or "subaxis" the term
2644 "item" will be used here
2645 - the baraxis stores a list of names be identify the items;
2646 the names might be of any time (strings, integers, etc.);
2647 the names can be printed as the titles for the items, but
2648 alternatively the names might be transformed by the texts
2649 dictionary, which maps a name to a text to be used to label
2650 the items in the painter
2651 - usually, there is only one subaxis, which is used as
2652 the subaxis for all items
2653 - alternatively it is also possible to use another baraxis
2654 as a multisubaxis; it is copied via the createsubaxis
2655 method whenever another subaxis is needed (by that a
2656 nested bar axis with a different number of subbars at
2657 each item can be created)
2658 - any axis can be a subaxis of a baraxis; if no subaxis
2659 is specified at all, the baraxis simulates a linear
2660 subaxis with a fixed range of 0 to 1
2661 - a splitaxis implements the _Iaxispos for its subaxes
2662 by inheritance from _subaxispos when the multisubaxis
2663 feature is turned on"""
2665 def __init__(self, subaxis=None, multisubaxis=None, title=None,
2666 dist=0.5, firstdist=None, lastdist=None, names=None,
2667 texts={}, painter=baraxispainter()):
2668 """initialize the instance
2669 - subaxis contains a axis to be used as the subaxis
2670 for all items
2671 - multisubaxis might contain another baraxis instance
2672 to be used to construct a new subaxis for each item;
2673 (by that a nested bar axis with a different number
2674 of subbars at each item can be created)
2675 - only one of subaxis or multisubaxis can be set; if neither
2676 of them is set, the baraxis behaves like having a linaxis
2677 as its subaxis with a fixed range 0 to 1
2678 - the title attribute contains the axis title as a string
2679 - the dist is a relsize to be used as the distance between
2680 the items
2681 - the firstdist and lastdist are the distance before the
2682 first and after the last item, respectively; when set
2683 to None (the default), 0.5*dist is used
2684 - names is a predefined list of names to identify the
2685 items; if set, the name list is fixed
2686 - texts is a dictionary transforming a name to a text in
2687 the painter; if a name isn't found in the dictionary
2688 it gets used itself
2689 - the relsize of the baraxis is the sum of the
2690 relsizes including all distances between the items"""
2691 self.dist = dist
2692 if firstdist is not None:
2693 self.firstdist = firstdist
2694 else:
2695 self.firstdist = 0.5 * dist
2696 if lastdist is not None:
2697 self.lastdist = lastdist
2698 else:
2699 self.lastdist = 0.5 * dist
2700 self.relsizes = None
2701 self.fixnames = 0
2702 self.names = []
2703 for name in helper.ensuresequence(names):
2704 self.setname(name)
2705 self.fixnames = names is not None
2706 self.multisubaxis = multisubaxis
2707 if self.multisubaxis is not None:
2708 if subaxis is not None:
2709 raise RuntimeError("either use subaxis or multisubaxis")
2710 self.subaxis = [self.createsubaxis() for name in self.names]
2711 else:
2712 self.subaxis = subaxis
2713 self.title = title
2714 self.fixnames = 0
2715 self.texts = texts
2716 self.painter = painter
2718 def createsubaxis(self):
2719 return baraxis(subaxis=self.multisubaxis.subaxis,
2720 multisubaxis=self.multisubaxis.multisubaxis,
2721 title=self.multisubaxis.title,
2722 dist=self.multisubaxis.dist,
2723 firstdist=self.multisubaxis.firstdist,
2724 lastdist=self.multisubaxis.lastdist,
2725 names=self.multisubaxis.names,
2726 texts=self.multisubaxis.texts,
2727 painter=self.multisubaxis.painter)
2729 def getrange(self):
2730 # TODO: we do not yet have a proper range handling for a baraxis
2731 return None
2733 def setrange(self, min=None, max=None):
2734 # TODO: we do not yet have a proper range handling for a baraxis
2735 raise RuntimeError("range handling for a baraxis is not implemented")
2737 def setname(self, name, *subnames):
2738 """add a name to identify an item at the baraxis
2739 - by using subnames, nested name definitions are
2740 possible
2741 - a style (or the user itself) might use this to
2742 insert new items into a baraxis
2743 - setting self.relsizes to None forces later recalculation"""
2744 if not self.fixnames:
2745 if name not in self.names:
2746 self.relsizes = None
2747 self.names.append(name)
2748 if self.multisubaxis is not None:
2749 self.subaxis.append(self.createsubaxis())
2750 if (not self.fixnames or name in self.names) and len(subnames):
2751 if self.multisubaxis is not None:
2752 if self.subaxis[self.names.index(name)].setname(*subnames):
2753 self.relsizes = None
2754 else:
2755 if self.subaxis.setname(*subnames):
2756 self.relsizes = None
2757 return self.relsizes is not None
2759 def updaterelsizes(self):
2760 # guess what it does: it recalculates relsize attribute
2761 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
2762 self.relsizes[-1] += self.lastdist - self.dist
2763 if self.multisubaxis is not None:
2764 subrelsize = 0
2765 for i in range(1, len(self.relsizes)):
2766 self.subaxis[i-1].updaterelsizes()
2767 subrelsize += self.subaxis[i-1].relsizes[-1]
2768 self.relsizes[i] += subrelsize
2769 else:
2770 if self.subaxis is None:
2771 subrelsize = 1
2772 else:
2773 self.subaxis.updaterelsizes()
2774 subrelsize = self.subaxis.relsizes[-1]
2775 for i in range(1, len(self.relsizes)):
2776 self.relsizes[i] += i * subrelsize
2778 def convert(self, value):
2779 """baraxis convert method
2780 - the value should be a list, where the first entry is
2781 a member of the names (set in the constructor or by the
2782 setname method); this first entry identifies an item in
2783 the baraxis
2784 - following values are passed to the appropriate subaxis
2785 convert method
2786 - when there is no subaxis, the convert method will behave
2787 like having a linaxis from 0 to 1 as subaxis"""
2788 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2789 if not self.relsizes:
2790 self.updaterelsizes()
2791 pos = self.names.index(value[0])
2792 if len(value) == 2:
2793 if self.subaxis is None:
2794 subvalue = value[1]
2795 else:
2796 if self.multisubaxis is not None:
2797 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
2798 else:
2799 subvalue = value[1] * self.subaxis.relsizes[-1]
2800 else:
2801 if self.multisubaxis is not None:
2802 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
2803 else:
2804 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
2805 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
2807 def baseline(self, x1=None, x2=None, axis=None):
2808 "None is returned -> subaxis should not paint any baselines"
2809 # TODO: quick hack ?!
2810 return None
2812 def vbaseline(self, v1=None, v2=None, axis=None):
2813 "None is returned -> subaxis should not paint any baselines"
2814 # TODO: quick hack ?!
2815 return None
2817 def finish(self, axispos, texrunner):
2818 if self.multisubaxis is not None:
2819 for name, subaxis in zip(self.names, self.subaxis):
2820 subaxis.vmin = self.convert((name, 0))
2821 subaxis.vmax = self.convert((name, 1))
2822 subaxis.baseaxispos = axispos
2823 subaxis.baseaxis = self
2824 return self.painter.paint(axispos, self)
2826 def createlinkaxis(self, **args):
2827 return linkbaraxis(self, **args)
2830 class linkbaraxis(linkaxis):
2831 """a baraxis linked to an already existing baraxis
2832 - inherits the access to a linked axis -- as before,
2833 basically only the painter is replaced
2834 - it must take care of the creation of linked axes of
2835 the subaxes"""
2837 __implements__ = _Iaxis
2839 def __init__(self, linkedaxis, painter=linkbaraxispainter()):
2840 """initializes the instance
2841 - it gets a axis this linkaxis is linked to
2842 - it gets a painter to be used for this linked axis"""
2843 linkaxis.__init__(self, linkedaxis, painter=painter)
2845 def finish(self, axispos, texrunner):
2846 if self.multisubaxis is not None:
2847 self.subaxis = [subaxis.createlinkaxis() for subaxis in self.linkedaxis.subaxis]
2848 for subaxis in self.subaxis:
2849 subaxis.baseaxispos = axispos
2850 subaxis.baseaxis = self
2851 elif self.linkedaxis.subaxis is not None:
2852 self.subaxis = self.linkedaxis.subaxis.createlinkaxis()
2853 self.subaxis.baseaxispos = axispos
2854 self.subaxis.baseaxis = self
2855 linkaxis.finish(self, axispos, texrunner)
2858 ################################################################################
2859 # graph key
2860 ################################################################################
2863 # g = graph.graphxy(key=graph.key())
2864 # g.addkey(graph.key(), ...)
2867 class key:
2869 def __init__(self, dist="0.2 cm", pos = "tr", hinside = 1, vinside = 1, hdist="0.6 cm", vdist="0.4 cm",
2870 symbolwidth="0.5 cm", symbolheight="0.25 cm", symbolspace="0.2 cm",
2871 textattrs=textmodule.vshift.mathaxis):
2872 self.dist_str = dist
2873 self.pos = pos
2874 self.hinside = hinside
2875 self.vinside = vinside
2876 self.hdist_str = hdist
2877 self.vdist_str = vdist
2878 self.symbolwidth_str = symbolwidth
2879 self.symbolheight_str = symbolheight
2880 self.symbolspace_str = symbolspace
2881 self.textattrs = textattrs
2882 self.plotinfos = None
2883 if self.pos in ("tr", "rt"):
2884 self.right = 1
2885 self.top = 1
2886 elif self.pos in ("br", "rb"):
2887 self.right = 1
2888 self.top = 0
2889 elif self.pos in ("tl", "lt"):
2890 self.right = 0
2891 self.top = 1
2892 elif self.pos in ("bl", "lb"):
2893 self.right = 0
2894 self.top = 0
2895 else:
2896 raise RuntimeError("invalid pos attribute")
2898 def setplotinfos(self, *plotinfos):
2899 """set the plotinfos to be used in the key
2900 - call it exactly once
2901 - plotinfo instances with title == None are ignored"""
2902 if self.plotinfos is not None:
2903 raise RuntimeError("setplotinfo is called multiple times")
2904 self.plotinfos = [plotinfo for plotinfo in plotinfos if plotinfo.data.title is not None]
2906 def dolayout(self, graph):
2907 "creates the layout of the key"
2908 self._dist = unit.topt(unit.length(self.dist_str, default_type="v"))
2909 self._hdist = unit.topt(unit.length(self.hdist_str, default_type="v"))
2910 self._vdist = unit.topt(unit.length(self.vdist_str, default_type="v"))
2911 self._symbolwidth = unit.topt(unit.length(self.symbolwidth_str, default_type="v"))
2912 self._symbolheight = unit.topt(unit.length(self.symbolheight_str, default_type="v"))
2913 self._symbolspace = unit.topt(unit.length(self.symbolspace_str, default_type="v"))
2914 self.titles = []
2915 for plotinfo in self.plotinfos:
2916 self.titles.append(graph.texrunner._text(0, 0, plotinfo.data.title, *helper.ensuresequence(self.textattrs)))
2917 box._tile(self.titles, self._dist, 0, -1)
2918 box._linealignequal(self.titles, self._symbolwidth + self._symbolspace, 1, 0)
2920 def bbox(self):
2921 """return a bbox for the key
2922 method should be called after dolayout"""
2923 result = bbox.bbox()
2924 for title in self.titles:
2925 result = result + title.bbox() + bbox._bbox(0, title.center[1] - 0.5 * self._symbolheight,
2926 0, title.center[1] + 0.5 * self._symbolheight)
2927 return result
2929 def paint(self, c, x, y):
2930 """paint the graph key into a canvas c at the position x and y (in postscript points)
2931 - method should be called after dolayout
2932 - the x, y alignment might be calculated by the graph using:
2933 - the bbox of the key as returned by the keys bbox method
2934 - the attributes _hdist, _vdist, hinside, and vinside of the key
2935 - the dimension and geometry of the graph"""
2936 sc = c.insert(canvas.canvas(trafomodule._translate(x, y)))
2937 for plotinfo, title in zip(self.plotinfos, self.titles):
2938 plotinfo.style.key(sc, 0, -0.5 * self._symbolheight + title.center[1],
2939 self._symbolwidth, self._symbolheight)
2940 sc.insert(title)
2943 ################################################################################
2944 # graph
2945 ################################################################################
2948 class lineaxispos(_axispos): # XXX: optimize by full implementation
2950 __implements__ = _Iaxispos
2952 def __init__(self, axis, x1, y1, x2, y2, fixtickdirection):
2953 """initializes the instance
2954 - only the convert method is needed from the axis a"""
2955 _axispos.__init__(self, axis)
2956 self.x1 = x1
2957 self.x2 = x2
2958 self.y1 = y1
2959 self.y2 = y2
2960 self.fixtickdirection = fixtickdirection
2962 def vbaseline(self, v1=None, v2=None):
2963 if v1 is None:
2964 v1 = 0 # XXX reversed?
2965 if v2 is None:
2966 v2 = 1
2967 return path.line((1-v1)*self.x1+v1*self.x2,
2968 (1-v1)*self.y1+v1*self.y2,
2969 (1-v2)*self.x1+v2*self.x2,
2970 (1-v2)*self.y1+v2*self.y2)
2972 def vgridline(self, v):
2973 # TODO: stub!
2974 raise RuntimeError("gridline implementation missing")
2976 def vtickpoint(self, v):
2977 return (1-v)*self.x1+v*self.x2, (1-v)*self.y1+v*self.y2
2979 def vtickdirection(self, v):
2980 return self.fixtickdirection
2983 class plotinfo:
2985 def __init__(self, data, style):
2986 self.data = data
2987 self.style = style
2991 class graphxy(canvas.canvas):
2993 Names = "x", "y"
2995 class axisposdata:
2997 def __init__(self, type, axispos, tickdirection):
2999 - type == 0: x-axis; type == 1: y-axis
3000 - _axispos is the y or x position of the x-axis or y-axis
3001 in postscript points, respectively
3002 - axispos is analogous to _axispos, but as a PyX length
3003 - dx and dy is the tick direction
3005 self.type = type
3006 self.axispos = axispos
3007 self._axispos = unit.topt(axispos)
3008 self.tickdirection = tickdirection
3010 def clipcanvas(self):
3011 return self.insert(canvas.canvas(canvas.clip(path._rect(self._xpos, self._ypos, self._width, self._height))))
3013 def plot(self, data, style=None):
3014 if self.haslayout:
3015 raise RuntimeError("layout setup was already performed")
3016 if style is None:
3017 if helper.issequence(data):
3018 raise RuntimeError("list plot needs an explicit style")
3019 if self.defaultstyle.has_key(data.defaultstyle):
3020 style = self.defaultstyle[data.defaultstyle].iterate()
3021 else:
3022 style = data.defaultstyle()
3023 self.defaultstyle[data.defaultstyle] = style
3024 plotinfos = []
3025 first = 1
3026 for d in helper.ensuresequence(data):
3027 if not first:
3028 style = style.iterate()
3029 first = 0
3030 if d is not None:
3031 d.setstyle(self, style)
3032 plotinfos.append(plotinfo(d, style))
3033 self.plotinfos.extend(plotinfos)
3034 if helper.issequence(data):
3035 return plotinfos
3036 return plotinfos[0]
3038 def addkey(self, key, *plotinfos):
3039 if self.haslayout:
3040 raise RuntimeError("layout setup was already performed")
3041 self.addkeys.append((key, plotinfos))
3043 def _pos(self, x, y, xaxis=None, yaxis=None):
3044 if xaxis is None:
3045 xaxis = self.axes["x"]
3046 if yaxis is None:
3047 yaxis = self.axes["y"]
3048 return self._xpos+xaxis.convert(x)*self._width, self._ypos+yaxis.convert(y)*self._height
3050 def pos(self, x, y, xaxis=None, yaxis=None):
3051 if xaxis is None:
3052 xaxis = self.axes["x"]
3053 if yaxis is None:
3054 yaxis = self.axes["y"]
3055 return self.xpos+xaxis.convert(x)*self.width, self.ypos+yaxis.convert(y)*self.height
3057 def _vpos(self, vx, vy):
3058 return self._xpos+vx*self._width, self._ypos+vy*self._height
3060 def vpos(self, vx, vy):
3061 return self.xpos+vx*self.width, self.ypos+vy*self.height
3063 # def xbaseline(self, x1=None, x2=None, axis=None):
3064 # if axis is None:
3065 # axis = self.axes["x"]
3066 # if x1 is not None:
3067 # v1 = axis.convert(x1)
3068 # else:
3069 # v1 = 0
3070 # if x2 is not None:
3071 # v2 = axis.convert(x2)
3072 # else:
3073 # v2 = 1
3074 # return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3075 # self._xpos+v2*self._width, axis.axisposdata._axispos)
3077 # def ybaseline(self, x1=None, x2=None, axis=None):
3078 # if axis is None:
3079 # axis = self.axes["y"]
3080 # if x1 is not None:
3081 # v1 = axis.convert(x1)
3082 # else:
3083 # v1 = 0
3084 # if x2 is not None:
3085 # v2 = axis.convert(x2)
3086 # else:
3087 # v2 = 1
3088 # return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3089 # axis.axisposdata._axispos, self._ypos+v2*self._height)
3091 # def vxbaseline(self, v1=None, v2=None, axis=None):
3092 # if axis is None:
3093 # axis = self.axes["x"]
3094 # if v1 is None:
3095 # v1 = 0
3096 # if v2 is None:
3097 # v2 = 1
3098 # return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3099 # self._xpos+v2*self._width, axis.axisposdata._axispos)
3101 # def vybaseline(self, v1=None, v2=None, axis=None):
3102 # if axis is None:
3103 # axis = self.axes["y"]
3104 # if v1 is None:
3105 # v1 = 0
3106 # if v2 is None:
3107 # v2 = 1
3108 # return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3109 # axis.axisposdata._axispos, self._ypos+v2*self._height)
3111 # def xgridline(self, x, axis=None):
3112 # if axis is None:
3113 # axis = self.axes["x"]
3114 # v = axis.convert(x)
3115 # return path._line(self._xpos+v*self._width, self._ypos,
3116 # self._xpos+v*self._width, self._ypos+self._height)
3118 # def ygridline(self, x, axis=None):
3119 # if axis is None:
3120 # axis = self.axes["y"]
3121 # v = axis.convert(x)
3122 # return path._line(self._xpos, self._ypos+v*self._height,
3123 # self._xpos+self._width, self._ypos+v*self._height)
3125 # def vxgridline(self, v, axis=None):
3126 # if axis is None:
3127 # axis = self.axes["x"]
3128 # return path._line(self._xpos+v*self._width, self._ypos,
3129 # self._xpos+v*self._width, self._ypos+self._height)
3131 # def vygridline(self, v, axis=None):
3132 # if axis is None:
3133 # axis = self.axes["y"]
3134 # return path._line(self._xpos, self._ypos+v*self._height,
3135 # self._xpos+self._width, self._ypos+v*self._height)
3137 # def _xtickpoint(self, x, axis=None):
3138 # if axis is None:
3139 # axis = self.axes["x"]
3140 # return self._xpos+axis.convert(x)*self._width, axis.axisposdata._axispos
3142 # def _ytickpoint(self, x, axis=None):
3143 # if axis is None:
3144 # axis = self.axes["y"]
3145 # return axis.axisposdata._axispos, self._ypos+axis.convert(x)*self._height
3147 # def xtickpoint(self, x, axis=None):
3148 # if axis is None:
3149 # axis = self.axes["x"]
3150 # return self.xpos+axis.convert(x)*self.width, axis.axisposdata.axispos
3152 # def ytickpoint(self, x, axis=None):
3153 # if axis is None:
3154 # axis = self.axes["y"]
3155 # return axis.axisposdata.axispos, self.ypos+axis.convert(x)*self.height
3157 # def _vxtickpoint(self, v, axis=None):
3158 # if axis is None:
3159 # axis = self.axes["x"]
3160 # return self._xpos+v*self._width, axis.axisposdata._axispos
3162 # def _vytickpoint(self, v, axis=None):
3163 # if axis is None:
3164 # axis = self.axes["y"]
3165 # return axis.axisposdata._axispos, self._ypos+v*self._height
3167 # def vxtickpoint(self, v, axis=None):
3168 # if axis is None:
3169 # axis = self.axes["x"]
3170 # return self.xpos+v*self.width, axis.axisposdata.axispos
3172 # def vytickpoint(self, v, axis=None):
3173 # if axis is None:
3174 # axis = self.axes["y"]
3175 # return axis.axisposdata.axispos, self.ypos+v*self.height
3177 # def xtickdirection(self, x, axis=None):
3178 # if axis is None:
3179 # axis = self.axes["x"]
3180 # return axis.axisposdata.tickdirection
3182 # def ytickdirection(self, x, axis=None):
3183 # if axis is None:
3184 # axis = self.axes["y"]
3185 # return axis.axisposdata.tickdirection
3187 # def vxtickdirection(self, v, axis=None):
3188 # if axis is None:
3189 # axis = self.axes["x"]
3190 # return axis.axisposdata.tickdirection
3192 # def vytickdirection(self, v, axis=None):
3193 # if axis is None:
3194 # axis = self.axes["y"]
3195 # return axis.axisposdata.tickdirection
3197 def _addpos(self, x, y, dx, dy):
3198 return x+dx, y+dy
3200 def _connect(self, x1, y1, x2, y2):
3201 return path._lineto(x2, y2)
3203 def keynum(self, key):
3204 try:
3205 while key[0] in string.letters:
3206 key = key[1:]
3207 return int(key)
3208 except IndexError:
3209 return 1
3211 def gatherranges(self):
3212 ranges = {}
3213 for plotinfo in self.plotinfos:
3214 pdranges = plotinfo.data.getranges()
3215 if pdranges is not None:
3216 for key in pdranges.keys():
3217 if key not in ranges.keys():
3218 ranges[key] = pdranges[key]
3219 else:
3220 ranges[key] = (min(ranges[key][0], pdranges[key][0]),
3221 max(ranges[key][1], pdranges[key][1]))
3222 # known ranges are also set as ranges for the axes
3223 for key, axis in self.axes.items():
3224 if key in ranges.keys():
3225 axis.setrange(*ranges[key])
3226 ranges[key] = axis.getrange()
3227 if ranges[key] is None:
3228 del ranges[key]
3229 return ranges
3231 def removedomethod(self, method):
3232 hadmethod = 0
3233 while 1:
3234 try:
3235 self.domethods.remove(method)
3236 hadmethod = 1
3237 except ValueError:
3238 return hadmethod
3240 def dolayout(self):
3241 if not self.removedomethod(self.dolayout): return
3242 self.haslayout = 1
3243 # create list of ranges
3244 # 1. gather ranges
3245 ranges = self.gatherranges()
3246 # 2. calculate additional ranges out of known ranges
3247 for plotinfo in self.plotinfos:
3248 plotinfo.data.setranges(ranges)
3249 # 3. gather ranges again
3250 self.gatherranges()
3251 # do the layout for all axes
3252 axesdist = unit.length(self.axesdist_str, default_type="v")
3253 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3254 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3255 xaxisextents = [0, 0]
3256 yaxisextents = [0, 0]
3257 needxaxisdist = [0, 0]
3258 needyaxisdist = [0, 0]
3259 items = list(self.axes.items())
3260 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3261 for key, axis in items:
3262 num = self.keynum(key)
3263 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3264 num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3265 if XPattern.match(key):
3266 if needxaxisdist[num2]:
3267 xaxisextents[num2] += axesdist
3268 self.axespos[key] = lineaxispos(self.axes[key],
3269 self.xpos,
3270 self.ypos + num2*self.height + num3*xaxisextents[num2],
3271 self.xpos + self.width,
3272 self.ypos + num2*self.height + num3*xaxisextents[num2],
3273 (0, num3))
3274 elif YPattern.match(key):
3275 if needyaxisdist[num2]:
3276 yaxisextents[num2] += axesdist
3277 self.axespos[key] = lineaxispos(self.axes[key],
3278 self.xpos + num2*self.width + num3*yaxisextents[num2],
3279 self.ypos,
3280 self.xpos + num2*self.width + num3*yaxisextents[num2],
3281 self.ypos + self.height,
3282 (num3, 0))
3283 else:
3284 raise ValueError("Axis key '%s' not allowed" % key)
3285 self.axescanvas[key] = axis.finish(self.axespos[key], self.texrunner)
3286 if XPattern.match(key):
3287 xaxisextents[num2] += self.axescanvas[key].extent
3288 needxaxisdist[num2] = 1
3289 if YPattern.match(key):
3290 yaxisextents[num2] += self.axescanvas[key].extent
3291 needyaxisdist[num2] = 1
3293 def dobackground(self):
3294 self.dolayout()
3295 if not self.removedomethod(self.dobackground): return
3296 if self.backgroundattrs is not None:
3297 self.draw(path._rect(self._xpos, self._ypos, self._width, self._height),
3298 *helper.ensuresequence(self.backgroundattrs))
3300 def doaxes(self):
3301 self.dolayout()
3302 if not self.removedomethod(self.doaxes): return
3303 for axiscanvas in self.axescanvas.values():
3304 self.insert(axiscanvas)
3306 def dodata(self):
3307 self.dolayout()
3308 if not self.removedomethod(self.dodata): return
3309 for plotinfo in self.plotinfos:
3310 plotinfo.data.draw(self)
3312 def _dokey(self, key, *plotinfos):
3313 key.setplotinfos(*plotinfos)
3314 key.dolayout(self)
3315 bbox = key.bbox()
3316 if key.right:
3317 if key.hinside:
3318 x = self._xpos + self._width - bbox.urx - key._hdist
3319 else:
3320 x = self._xpos + self._width - bbox.llx + key._hdist
3321 else:
3322 if key.hinside:
3323 x = self._xpos - bbox.llx + key._hdist
3324 else:
3325 x = self._xpos - bbox.urx - key._hdist
3326 if key.top:
3327 if key.vinside:
3328 y = self._ypos + self._height - bbox.ury - key._vdist
3329 else:
3330 y = self._ypos + self._height - bbox.lly + key._vdist
3331 else:
3332 if key.vinside:
3333 y = self._ypos - bbox.lly + key._vdist
3334 else:
3335 y = self._ypos - bbox.ury - key._vdist
3336 key.paint(self, x, y)
3338 def dokey(self):
3339 self.dolayout()
3340 if not self.removedomethod(self.dokey): return
3341 if self.key is not None:
3342 self._dokey(self.key, *self.plotinfos)
3343 for key, plotinfos in self.addkeys:
3344 self._dokey(key, *plotinfos)
3346 def finish(self):
3347 while len(self.domethods):
3348 self.domethods[0]()
3350 def initwidthheight(self, width, height, ratio):
3351 if (width is not None) and (height is None):
3352 self.width = unit.length(width)
3353 self.height = (1.0/ratio) * self.width
3354 elif (height is not None) and (width is None):
3355 self.height = unit.length(height)
3356 self.width = ratio * self.height
3357 else:
3358 self.width = unit.length(width)
3359 self.height = unit.length(height)
3360 self._width = unit.topt(self.width)
3361 self._height = unit.topt(self.height)
3362 if self._width <= 0: raise ValueError("width <= 0")
3363 if self._height <= 0: raise ValueError("height <= 0")
3365 def initaxes(self, axes, addlinkaxes=0):
3366 for key in self.Names:
3367 if not axes.has_key(key):
3368 axes[key] = linaxis()
3369 elif axes[key] is None:
3370 del axes[key]
3371 if addlinkaxes:
3372 if not axes.has_key(key + "2") and axes.has_key(key):
3373 axes[key + "2"] = axes[key].createlinkaxis()
3374 elif axes[key + "2"] is None:
3375 del axes[key + "2"]
3376 self.axes = axes
3378 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
3379 key=None, backgroundattrs=None, axesdist="0.8 cm", **axes):
3380 canvas.canvas.__init__(self)
3381 self.xpos = unit.length(xpos)
3382 self.ypos = unit.length(ypos)
3383 self._xpos = unit.topt(self.xpos)
3384 self._ypos = unit.topt(self.ypos)
3385 self.initwidthheight(width, height, ratio)
3386 self.initaxes(axes, 1)
3387 self.axescanvas = {}
3388 self.axespos = {}
3389 self.key = key
3390 self.backgroundattrs = backgroundattrs
3391 self.axesdist_str = axesdist
3392 self.plotinfos = []
3393 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata, self.dokey]
3394 self.haslayout = 0
3395 self.defaultstyle = {}
3396 self.addkeys = []
3398 def bbox(self):
3399 self.finish()
3400 return canvas.canvas.bbox(self)
3402 def write(self, file):
3403 self.finish()
3404 canvas.canvas.write(self, file)
3408 # some thoughts, but deferred right now
3410 # class graphxyz(graphxy):
3412 # Names = "x", "y", "z"
3414 # def _vxtickpoint(self, axis, v):
3415 # return self._vpos(v, axis.vypos, axis.vzpos)
3417 # def _vytickpoint(self, axis, v):
3418 # return self._vpos(axis.vxpos, v, axis.vzpos)
3420 # def _vztickpoint(self, axis, v):
3421 # return self._vpos(axis.vxpos, axis.vypos, v)
3423 # def vxtickdirection(self, axis, v):
3424 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
3425 # x2, y2 = self._vpos(v, 0.5, 0)
3426 # dx, dy = x1 - x2, y1 - y2
3427 # norm = math.sqrt(dx*dx + dy*dy)
3428 # return dx/norm, dy/norm
3430 # def vytickdirection(self, axis, v):
3431 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
3432 # x2, y2 = self._vpos(0.5, v, 0)
3433 # dx, dy = x1 - x2, y1 - y2
3434 # norm = math.sqrt(dx*dx + dy*dy)
3435 # return dx/norm, dy/norm
3437 # def vztickdirection(self, axis, v):
3438 # return -1, 0
3439 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
3440 # x2, y2 = self._vpos(0.5, 0.5, v)
3441 # dx, dy = x1 - x2, y1 - y2
3442 # norm = math.sqrt(dx*dx + dy*dy)
3443 # return dx/norm, dy/norm
3445 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3446 # if xaxis is None: xaxis = self.axes["x"]
3447 # if yaxis is None: yaxis = self.axes["y"]
3448 # if zaxis is None: zaxis = self.axes["z"]
3449 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3451 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3452 # if xaxis is None: xaxis = self.axes["x"]
3453 # if yaxis is None: yaxis = self.axes["y"]
3454 # if zaxis is None: zaxis = self.axes["z"]
3455 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3457 # def _vpos(self, vx, vy, vz):
3458 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
3459 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
3460 # + self.a[2]*self.b[0]*(y-self.eye[1])
3461 # + self.a[1]*self.b[2]*(x-self.eye[0])
3462 # - self.a[2]*self.b[1]*(x-self.eye[0])
3463 # - self.a[0]*self.b[2]*(y-self.eye[1])
3464 # - self.a[1]*self.b[0]*(z-self.eye[2]))
3465 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
3466 # + self.eye[2]*self.b[0]*(y-self.eye[1])
3467 # + self.eye[1]*self.b[2]*(x-self.eye[0])
3468 # - self.eye[2]*self.b[1]*(x-self.eye[0])
3469 # - self.eye[0]*self.b[2]*(y-self.eye[1])
3470 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
3471 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
3472 # + self.a[2]*self.eye[0]*(y-self.eye[1])
3473 # + self.a[1]*self.eye[2]*(x-self.eye[0])
3474 # - self.a[2]*self.eye[1]*(x-self.eye[0])
3475 # - self.a[0]*self.eye[2]*(y-self.eye[1])
3476 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
3477 # return da/d0 + self._xpos, db/d0 + self._ypos
3479 # def vpos(self, vx, vy, vz):
3480 # tx, ty = self._vpos(vx, vy, vz)
3481 # return unit.t_pt(tx), unit.t_pt(ty)
3483 # def xbaseline(self, axis, x1, x2, xaxis=None):
3484 # if xaxis is None: xaxis = self.axes["x"]
3485 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2))
3487 # def ybaseline(self, axis, y1, y2, yaxis=None):
3488 # if yaxis is None: yaxis = self.axes["y"]
3489 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2))
3491 # def zbaseline(self, axis, z1, z2, zaxis=None):
3492 # if zaxis is None: zaxis = self.axes["z"]
3493 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2))
3495 # def vxbaseline(self, axis, v1, v2):
3496 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
3497 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
3498 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
3499 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
3501 # def vybaseline(self, axis, v1, v2):
3502 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
3503 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
3504 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
3505 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
3507 # def vzbaseline(self, axis, v1, v2):
3508 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
3509 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
3510 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
3511 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
3513 # def xgridpath(self, x, xaxis=None):
3514 # assert 0
3515 # if xaxis is None: xaxis = self.axes["x"]
3516 # v = xaxis.convert(x)
3517 # return path._line(self._xpos+v*self._width, self._ypos,
3518 # self._xpos+v*self._width, self._ypos+self._height)
3520 # def ygridpath(self, y, yaxis=None):
3521 # assert 0
3522 # if yaxis is None: yaxis = self.axes["y"]
3523 # v = yaxis.convert(y)
3524 # return path._line(self._xpos, self._ypos+v*self._height,
3525 # self._xpos+self._width, self._ypos+v*self._height)
3527 # def zgridpath(self, z, zaxis=None):
3528 # assert 0
3529 # if zaxis is None: zaxis = self.axes["z"]
3530 # v = zaxis.convert(z)
3531 # return path._line(self._xpos, self._zpos+v*self._height,
3532 # self._xpos+self._width, self._zpos+v*self._height)
3534 # def vxgridpath(self, v):
3535 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
3536 # path._lineto(*self._vpos(v, 0, 1)),
3537 # path._lineto(*self._vpos(v, 1, 1)),
3538 # path._lineto(*self._vpos(v, 1, 0)),
3539 # path.closepath())
3541 # def vygridpath(self, v):
3542 # return path.path(path._moveto(*self._vpos(0, v, 0)),
3543 # path._lineto(*self._vpos(0, v, 1)),
3544 # path._lineto(*self._vpos(1, v, 1)),
3545 # path._lineto(*self._vpos(1, v, 0)),
3546 # path.closepath())
3548 # def vzgridpath(self, v):
3549 # return path.path(path._moveto(*self._vpos(0, 0, v)),
3550 # path._lineto(*self._vpos(0, 1, v)),
3551 # path._lineto(*self._vpos(1, 1, v)),
3552 # path._lineto(*self._vpos(1, 0, v)),
3553 # path.closepath())
3555 # def _addpos(self, x, y, dx, dy):
3556 # assert 0
3557 # return x+dx, y+dy
3559 # def _connect(self, x1, y1, x2, y2):
3560 # assert 0
3561 # return path._lineto(x2, y2)
3563 # def doaxes(self):
3564 # self.dolayout()
3565 # if not self.removedomethod(self.doaxes): return
3566 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
3567 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3568 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3569 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[2])
3570 # items = list(self.axes.items())
3571 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3572 # for key, axis in items:
3573 # num = self.keynum(key)
3574 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3575 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3576 # if XPattern.match(key):
3577 # axis.vypos = 0
3578 # axis.vzpos = 0
3579 # axis._vtickpoint = self._vxtickpoint
3580 # axis.vgridpath = self.vxgridpath
3581 # axis.vbaseline = self.vxbaseline
3582 # axis.vtickdirection = self.vxtickdirection
3583 # elif YPattern.match(key):
3584 # axis.vxpos = 0
3585 # axis.vzpos = 0
3586 # axis._vtickpoint = self._vytickpoint
3587 # axis.vgridpath = self.vygridpath
3588 # axis.vbaseline = self.vybaseline
3589 # axis.vtickdirection = self.vytickdirection
3590 # elif ZPattern.match(key):
3591 # axis.vxpos = 0
3592 # axis.vypos = 0
3593 # axis._vtickpoint = self._vztickpoint
3594 # axis.vgridpath = self.vzgridpath
3595 # axis.vbaseline = self.vzbaseline
3596 # axis.vtickdirection = self.vztickdirection
3597 # else:
3598 # raise ValueError("Axis key '%s' not allowed" % key)
3599 # if axis.painter is not None:
3600 # axis.dopaint(self)
3601 # # if XPattern.match(key):
3602 # # self._xaxisextents[num2] += axis._extent
3603 # # needxaxisdist[num2] = 1
3604 # # if YPattern.match(key):
3605 # # self._yaxisextents[num2] += axis._extent
3606 # # needyaxisdist[num2] = 1
3608 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
3609 # phi=30, theta=30, distance=1,
3610 # backgroundattrs=None, axesdist="0.8 cm", **axes):
3611 # canvas.canvas.__init__(self)
3612 # self.tex = tex
3613 # self.xpos = xpos
3614 # self.ypos = ypos
3615 # self._xpos = unit.topt(xpos)
3616 # self._ypos = unit.topt(ypos)
3617 # self._width = unit.topt(width)
3618 # self._height = unit.topt(height)
3619 # self._depth = unit.topt(depth)
3620 # self.width = width
3621 # self.height = height
3622 # self.depth = depth
3623 # if self._width <= 0: raise ValueError("width < 0")
3624 # if self._height <= 0: raise ValueError("height < 0")
3625 # if self._depth <= 0: raise ValueError("height < 0")
3626 # self._distance = distance*math.sqrt(self._width*self._width+
3627 # self._height*self._height+
3628 # self._depth*self._depth)
3629 # phi *= -math.pi/180
3630 # theta *= math.pi/180
3631 # self.a = (-math.sin(phi), math.cos(phi), 0)
3632 # self.b = (-math.cos(phi)*math.sin(theta),
3633 # -math.sin(phi)*math.sin(theta),
3634 # math.cos(theta))
3635 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
3636 # self._distance*math.sin(phi)*math.cos(theta),
3637 # self._distance*math.sin(theta))
3638 # self.initaxes(axes)
3639 # self.axesdist_str = axesdist
3640 # self.backgroundattrs = backgroundattrs
3642 # self.data = []
3643 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
3644 # self.haslayout = 0
3645 # self.defaultstyle = {}
3647 # def bbox(self):
3648 # self.finish()
3649 # return bbox._bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
3652 ################################################################################
3653 # attr changers
3654 ################################################################################
3657 #class _Ichangeattr:
3658 # """attribute changer
3659 # is an iterator for attributes where an attribute
3660 # is not refered by just a number (like for a sequence),
3661 # but also by the number of attributes requested
3662 # by calls of the next method (like for an color palette)
3663 # (you should ensure to call all needed next before the attr)
3665 # the attribute itself is implemented by overloading the _attr method"""
3667 # def attr(self):
3668 # "get an attribute"
3670 # def next(self):
3671 # "get an attribute changer for the next attribute"
3674 class _changeattr: pass
3677 class changeattr(_changeattr):
3679 def __init__(self):
3680 self.counter = 1
3682 def getattr(self):
3683 return self.attr(0)
3685 def iterate(self):
3686 newindex = self.counter
3687 self.counter += 1
3688 return refattr(self, newindex)
3691 class refattr(_changeattr):
3693 def __init__(self, ref, index):
3694 self.ref = ref
3695 self.index = index
3697 def getattr(self):
3698 return self.ref.attr(self.index)
3700 def iterate(self):
3701 return self.ref.iterate()
3704 # helper routines for a using attrs
3706 def _getattr(attr):
3707 "get attr out of a attr/changeattr"
3708 if isinstance(attr, _changeattr):
3709 return attr.getattr()
3710 return attr
3713 def _getattrs(attrs):
3714 "get attrs out of a list of attr/changeattr"
3715 if attrs is not None:
3716 result = []
3717 for attr in helper.ensuresequence(attrs):
3718 if isinstance(attr, _changeattr):
3719 attr = attr.getattr()
3720 if attr is not None:
3721 result.append(attr)
3722 if len(result) or not len(attrs):
3723 return result
3726 def _iterateattr(attr):
3727 "perform next to a attr/changeattr"
3728 if isinstance(attr, _changeattr):
3729 return attr.iterate()
3730 return attr
3733 def _iterateattrs(attrs):
3734 "perform next to a list of attr/changeattr"
3735 if attrs is not None:
3736 result = []
3737 for attr in helper.ensuresequence(attrs):
3738 if isinstance(attr, _changeattr):
3739 result.append(attr.iterate())
3740 else:
3741 result.append(attr)
3742 return result
3745 class changecolor(changeattr):
3747 def __init__(self, palette):
3748 changeattr.__init__(self)
3749 self.palette = palette
3751 def attr(self, index):
3752 if self.counter != 1:
3753 return self.palette.getcolor(index/float(self.counter-1))
3754 else:
3755 return self.palette.getcolor(0)
3758 class _changecolorgray(changecolor):
3760 def __init__(self, palette=color.palette.Gray):
3761 changecolor.__init__(self, palette)
3763 _changecolorgrey = _changecolorgray
3766 class _changecolorreversegray(changecolor):
3768 def __init__(self, palette=color.palette.ReverseGray):
3769 changecolor.__init__(self, palette)
3771 _changecolorreversegrey = _changecolorreversegray
3774 class _changecolorredblack(changecolor):
3776 def __init__(self, palette=color.palette.RedBlack):
3777 changecolor.__init__(self, palette)
3780 class _changecolorblackred(changecolor):
3782 def __init__(self, palette=color.palette.BlackRed):
3783 changecolor.__init__(self, palette)
3786 class _changecolorredwhite(changecolor):
3788 def __init__(self, palette=color.palette.RedWhite):
3789 changecolor.__init__(self, palette)
3792 class _changecolorwhitered(changecolor):
3794 def __init__(self, palette=color.palette.WhiteRed):
3795 changecolor.__init__(self, palette)
3798 class _changecolorgreenblack(changecolor):
3800 def __init__(self, palette=color.palette.GreenBlack):
3801 changecolor.__init__(self, palette)
3804 class _changecolorblackgreen(changecolor):
3806 def __init__(self, palette=color.palette.BlackGreen):
3807 changecolor.__init__(self, palette)
3810 class _changecolorgreenwhite(changecolor):
3812 def __init__(self, palette=color.palette.GreenWhite):
3813 changecolor.__init__(self, palette)
3816 class _changecolorwhitegreen(changecolor):
3818 def __init__(self, palette=color.palette.WhiteGreen):
3819 changecolor.__init__(self, palette)
3822 class _changecolorblueblack(changecolor):
3824 def __init__(self, palette=color.palette.BlueBlack):
3825 changecolor.__init__(self, palette)
3828 class _changecolorblackblue(changecolor):
3830 def __init__(self, palette=color.palette.BlackBlue):
3831 changecolor.__init__(self, palette)
3834 class _changecolorbluewhite(changecolor):
3836 def __init__(self, palette=color.palette.BlueWhite):
3837 changecolor.__init__(self, palette)
3840 class _changecolorwhiteblue(changecolor):
3842 def __init__(self, palette=color.palette.WhiteBlue):
3843 changecolor.__init__(self, palette)
3846 class _changecolorredgreen(changecolor):
3848 def __init__(self, palette=color.palette.RedGreen):
3849 changecolor.__init__(self, palette)
3852 class _changecolorredblue(changecolor):
3854 def __init__(self, palette=color.palette.RedBlue):
3855 changecolor.__init__(self, palette)
3858 class _changecolorgreenred(changecolor):
3860 def __init__(self, palette=color.palette.GreenRed):
3861 changecolor.__init__(self, palette)
3864 class _changecolorgreenblue(changecolor):
3866 def __init__(self, palette=color.palette.GreenBlue):
3867 changecolor.__init__(self, palette)
3870 class _changecolorbluered(changecolor):
3872 def __init__(self, palette=color.palette.BlueRed):
3873 changecolor.__init__(self, palette)
3876 class _changecolorbluegreen(changecolor):
3878 def __init__(self, palette=color.palette.BlueGreen):
3879 changecolor.__init__(self, palette)
3882 class _changecolorrainbow(changecolor):
3884 def __init__(self, palette=color.palette.Rainbow):
3885 changecolor.__init__(self, palette)
3888 class _changecolorreverserainbow(changecolor):
3890 def __init__(self, palette=color.palette.ReverseRainbow):
3891 changecolor.__init__(self, palette)
3894 class _changecolorhue(changecolor):
3896 def __init__(self, palette=color.palette.Hue):
3897 changecolor.__init__(self, palette)
3900 class _changecolorreversehue(changecolor):
3902 def __init__(self, palette=color.palette.ReverseHue):
3903 changecolor.__init__(self, palette)
3906 changecolor.Gray = _changecolorgray
3907 changecolor.Grey = _changecolorgrey
3908 changecolor.Reversegray = _changecolorreversegray
3909 changecolor.Reversegrey = _changecolorreversegrey
3910 changecolor.RedBlack = _changecolorredblack
3911 changecolor.BlackRed = _changecolorblackred
3912 changecolor.RedWhite = _changecolorredwhite
3913 changecolor.WhiteRed = _changecolorwhitered
3914 changecolor.GreenBlack = _changecolorgreenblack
3915 changecolor.BlackGreen = _changecolorblackgreen
3916 changecolor.GreenWhite = _changecolorgreenwhite
3917 changecolor.WhiteGreen = _changecolorwhitegreen
3918 changecolor.BlueBlack = _changecolorblueblack
3919 changecolor.BlackBlue = _changecolorblackblue
3920 changecolor.BlueWhite = _changecolorbluewhite
3921 changecolor.WhiteBlue = _changecolorwhiteblue
3922 changecolor.RedGreen = _changecolorredgreen
3923 changecolor.RedBlue = _changecolorredblue
3924 changecolor.GreenRed = _changecolorgreenred
3925 changecolor.GreenBlue = _changecolorgreenblue
3926 changecolor.BlueRed = _changecolorbluered
3927 changecolor.BlueGreen = _changecolorbluegreen
3928 changecolor.Rainbow = _changecolorrainbow
3929 changecolor.ReverseRainbow = _changecolorreverserainbow
3930 changecolor.Hue = _changecolorhue
3931 changecolor.ReverseHue = _changecolorreversehue
3934 class changesequence(changeattr):
3935 "cycles through a list"
3937 def __init__(self, *sequence):
3938 changeattr.__init__(self)
3939 if not len(sequence):
3940 sequence = self.defaultsequence
3941 self.sequence = sequence
3943 def attr(self, index):
3944 return self.sequence[index % len(self.sequence)]
3947 class changelinestyle(changesequence):
3948 defaultsequence = (style.linestyle.solid,
3949 style.linestyle.dashed,
3950 style.linestyle.dotted,
3951 style.linestyle.dashdotted)
3954 class changestrokedfilled(changesequence):
3955 defaultsequence = (deco.stroked(), deco.filled())
3958 class changefilledstroked(changesequence):
3959 defaultsequence = (deco.filled(), deco.stroked())
3963 ################################################################################
3964 # styles
3965 ################################################################################
3968 class symbol:
3970 def cross(self, x, y):
3971 return (path._moveto(x-0.5*self._size, y-0.5*self._size),
3972 path._lineto(x+0.5*self._size, y+0.5*self._size),
3973 path._moveto(x-0.5*self._size, y+0.5*self._size),
3974 path._lineto(x+0.5*self._size, y-0.5*self._size))
3976 def plus(self, x, y):
3977 return (path._moveto(x-0.707106781*self._size, y),
3978 path._lineto(x+0.707106781*self._size, y),
3979 path._moveto(x, y-0.707106781*self._size),
3980 path._lineto(x, y+0.707106781*self._size))
3982 def square(self, x, y):
3983 return (path._moveto(x-0.5*self._size, y-0.5 * self._size),
3984 path._lineto(x+0.5*self._size, y-0.5 * self._size),
3985 path._lineto(x+0.5*self._size, y+0.5 * self._size),
3986 path._lineto(x-0.5*self._size, y+0.5 * self._size),
3987 path.closepath())
3989 def triangle(self, x, y):
3990 return (path._moveto(x-0.759835685*self._size, y-0.438691337*self._size),
3991 path._lineto(x+0.759835685*self._size, y-0.438691337*self._size),
3992 path._lineto(x, y+0.877382675*self._size),
3993 path.closepath())
3995 def circle(self, x, y):
3996 return (path._arc(x, y, 0.564189583*self._size, 0, 360),
3997 path.closepath())
3999 def diamond(self, x, y):
4000 return (path._moveto(x-0.537284965*self._size, y),
4001 path._lineto(x, y-0.930604859*self._size),
4002 path._lineto(x+0.537284965*self._size, y),
4003 path._lineto(x, y+0.930604859*self._size),
4004 path.closepath())
4006 def __init__(self, symbol=helper.nodefault,
4007 size="0.2 cm", symbolattrs=deco.stroked(),
4008 errorscale=0.5, errorbarattrs=(),
4009 lineattrs=None):
4010 self.size_str = size
4011 if symbol is helper.nodefault:
4012 self._symbol = changesymbol.cross()
4013 else:
4014 self._symbol = symbol
4015 self._symbolattrs = symbolattrs
4016 self.errorscale = errorscale
4017 self._errorbarattrs = errorbarattrs
4018 self._lineattrs = lineattrs
4020 def iteratedict(self):
4021 result = {}
4022 result["symbol"] = _iterateattr(self._symbol)
4023 result["size"] = _iterateattr(self.size_str)
4024 result["symbolattrs"] = _iterateattrs(self._symbolattrs)
4025 result["errorscale"] = _iterateattr(self.errorscale)
4026 result["errorbarattrs"] = _iterateattrs(self._errorbarattrs)
4027 result["lineattrs"] = _iterateattrs(self._lineattrs)
4028 return result
4030 def iterate(self):
4031 return symbol(**self.iteratedict())
4033 def othercolumnkey(self, key, index):
4034 raise ValueError("unsuitable key '%s'" % key)
4036 def setcolumns(self, graph, columns):
4037 def checkpattern(key, index, pattern, iskey, isindex):
4038 if key is not None:
4039 match = pattern.match(key)
4040 if match:
4041 if isindex is not None: raise ValueError("multiple key specification")
4042 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4043 key = None
4044 iskey = match.groups()[0]
4045 isindex = index
4046 return key, iskey, isindex
4048 self.xi = self.xmini = self.xmaxi = None
4049 self.dxi = self.dxmini = self.dxmaxi = None
4050 self.yi = self.ymini = self.ymaxi = None
4051 self.dyi = self.dymini = self.dymaxi = None
4052 self.xkey = self.ykey = None
4053 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4054 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4055 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4056 XMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
4057 YMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
4058 XMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
4059 YMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
4060 DXPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4061 DYPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4062 DXMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
4063 DYMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
4064 DXMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
4065 DYMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
4066 for key, index in columns.items():
4067 key, self.xkey, self.xi = checkpattern(key, index, XPattern, self.xkey, self.xi)
4068 key, self.ykey, self.yi = checkpattern(key, index, YPattern, self.ykey, self.yi)
4069 key, self.xkey, self.xmini = checkpattern(key, index, XMinPattern, self.xkey, self.xmini)
4070 key, self.ykey, self.ymini = checkpattern(key, index, YMinPattern, self.ykey, self.ymini)
4071 key, self.xkey, self.xmaxi = checkpattern(key, index, XMaxPattern, self.xkey, self.xmaxi)
4072 key, self.ykey, self.ymaxi = checkpattern(key, index, YMaxPattern, self.ykey, self.ymaxi)
4073 key, self.xkey, self.dxi = checkpattern(key, index, DXPattern, self.xkey, self.dxi)
4074 key, self.ykey, self.dyi = checkpattern(key, index, DYPattern, self.ykey, self.dyi)
4075 key, self.xkey, self.dxmini = checkpattern(key, index, DXMinPattern, self.xkey, self.dxmini)
4076 key, self.ykey, self.dymini = checkpattern(key, index, DYMinPattern, self.ykey, self.dymini)
4077 key, self.xkey, self.dxmaxi = checkpattern(key, index, DXMaxPattern, self.xkey, self.dxmaxi)
4078 key, self.ykey, self.dymaxi = checkpattern(key, index, DYMaxPattern, self.ykey, self.dymaxi)
4079 if key is not None:
4080 self.othercolumnkey(key, index)
4081 if None in (self.xkey, self.ykey): raise ValueError("incomplete axis specification")
4082 if (len(filter(None, (self.xmini, self.dxmini, self.dxi))) > 1 or
4083 len(filter(None, (self.ymini, self.dymini, self.dyi))) > 1 or
4084 len(filter(None, (self.xmaxi, self.dxmaxi, self.dxi))) > 1 or
4085 len(filter(None, (self.ymaxi, self.dymaxi, self.dyi))) > 1):
4086 raise ValueError("multiple errorbar definition")
4087 if ((self.xi is None and self.dxi is not None) or
4088 (self.yi is None and self.dyi is not None) or
4089 (self.xi is None and self.dxmini is not None) or
4090 (self.yi is None and self.dymini is not None) or
4091 (self.xi is None and self.dxmaxi is not None) or
4092 (self.yi is None and self.dymaxi is not None)):
4093 raise ValueError("errorbar definition start value missing")
4094 self.xaxis = graph.axes[self.xkey]
4095 self.yaxis = graph.axes[self.ykey]
4097 def minmidmax(self, point, i, mini, maxi, di, dmini, dmaxi):
4098 min = max = mid = None
4099 try:
4100 mid = point[i] + 0.0
4101 except (TypeError, ValueError):
4102 pass
4103 try:
4104 if di is not None: min = point[i] - point[di]
4105 elif dmini is not None: min = point[i] - point[dmini]
4106 elif mini is not None: min = point[mini] + 0.0
4107 except (TypeError, ValueError):
4108 pass
4109 try:
4110 if di is not None: max = point[i] + point[di]
4111 elif dmaxi is not None: max = point[i] + point[dmaxi]
4112 elif maxi is not None: max = point[maxi] + 0.0
4113 except (TypeError, ValueError):
4114 pass
4115 if mid is not None:
4116 if min is not None and min > mid: raise ValueError("minimum error in errorbar")
4117 if max is not None and max < mid: raise ValueError("maximum error in errorbar")
4118 else:
4119 if min is not None and max is not None and min > max: raise ValueError("minimum/maximum error in errorbar")
4120 return min, mid, max
4122 def keyrange(self, points, i, mini, maxi, di, dmini, dmaxi):
4123 allmin = allmax = None
4124 if filter(None, (mini, maxi, di, dmini, dmaxi)) is not None:
4125 for point in points:
4126 min, mid, max = self.minmidmax(point, i, mini, maxi, di, dmini, dmaxi)
4127 if min is not None and (allmin is None or min < allmin): allmin = min
4128 if mid is not None and (allmin is None or mid < allmin): allmin = mid
4129 if mid is not None and (allmax is None or mid > allmax): allmax = mid
4130 if max is not None and (allmax is None or max > allmax): allmax = max
4131 else:
4132 for point in points:
4133 try:
4134 value = point[i] + 0.0
4135 if allmin is None or point[i] < allmin: allmin = point[i]
4136 if allmax is None or point[i] > allmax: allmax = point[i]
4137 except (TypeError, ValueError):
4138 pass
4139 return allmin, allmax
4141 def getranges(self, points):
4142 xmin, xmax = self.keyrange(points, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4143 ymin, ymax = self.keyrange(points, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4144 return {self.xkey: (xmin, xmax), self.ykey: (ymin, ymax)}
4146 def _drawerrorbar(self, graph, topleft, top, topright,
4147 left, center, right,
4148 bottomleft, bottom, bottomright, point=None):
4149 if left is not None:
4150 if right is not None:
4151 left1 = graph._addpos(*(left+(0, -self._errorsize)))
4152 left2 = graph._addpos(*(left+(0, self._errorsize)))
4153 right1 = graph._addpos(*(right+(0, -self._errorsize)))
4154 right2 = graph._addpos(*(right+(0, self._errorsize)))
4155 graph.stroke(path.path(path._moveto(*left1),
4156 graph._connect(*(left1+left2)),
4157 path._moveto(*left),
4158 graph._connect(*(left+right)),
4159 path._moveto(*right1),
4160 graph._connect(*(right1+right2))),
4161 *self.errorbarattrs)
4162 elif center is not None:
4163 left1 = graph._addpos(*(left+(0, -self._errorsize)))
4164 left2 = graph._addpos(*(left+(0, self._errorsize)))
4165 graph.stroke(path.path(path._moveto(*left1),
4166 graph._connect(*(left1+left2)),
4167 path._moveto(*left),
4168 graph._connect(*(left+center))),
4169 *self.errorbarattrs)
4170 else:
4171 left1 = graph._addpos(*(left+(0, -self._errorsize)))
4172 left2 = graph._addpos(*(left+(0, self._errorsize)))
4173 left3 = graph._addpos(*(left+(self._errorsize, 0)))
4174 graph.stroke(path.path(path._moveto(*left1),
4175 graph._connect(*(left1+left2)),
4176 path._moveto(*left),
4177 graph._connect(*(left+left3))),
4178 *self.errorbarattrs)
4179 if right is not None and left is None:
4180 if center is not None:
4181 right1 = graph._addpos(*(right+(0, -self._errorsize)))
4182 right2 = graph._addpos(*(right+(0, self._errorsize)))
4183 graph.stroke(path.path(path._moveto(*right1),
4184 graph._connect(*(right1+right2)),
4185 path._moveto(*right),
4186 graph._connect(*(right+center))),
4187 *self.errorbarattrs)
4188 else:
4189 right1 = graph._addpos(*(right+(0, -self._errorsize)))
4190 right2 = graph._addpos(*(right+(0, self._errorsize)))
4191 right3 = graph._addpos(*(right+(-self._errorsize, 0)))
4192 graph.stroke(path.path(path._moveto(*right1),
4193 graph._connect(*(right1+right2)),
4194 path._moveto(*right),
4195 graph._connect(*(right+right3))),
4196 *self.errorbarattrs)
4198 if bottom is not None:
4199 if top is not None:
4200 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
4201 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
4202 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
4203 top2 = graph._addpos(*(top+(self._errorsize, 0)))
4204 graph.stroke(path.path(path._moveto(*bottom1),
4205 graph._connect(*(bottom1+bottom2)),
4206 path._moveto(*bottom),
4207 graph._connect(*(bottom+top)),
4208 path._moveto(*top1),
4209 graph._connect(*(top1+top2))),
4210 *self.errorbarattrs)
4211 elif center is not None:
4212 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
4213 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
4214 graph.stroke(path.path(path._moveto(*bottom1),
4215 graph._connect(*(bottom1+bottom2)),
4216 path._moveto(*bottom),
4217 graph._connect(*(bottom+center))),
4218 *self.errorbarattrs)
4219 else:
4220 bottom1 = graph._addpos(*(bottom+(-self._errorsize, 0)))
4221 bottom2 = graph._addpos(*(bottom+(self._errorsize, 0)))
4222 bottom3 = graph._addpos(*(bottom+(0, self._errorsize)))
4223 graph.stroke(path.path(path._moveto(*bottom1),
4224 graph._connect(*(bottom1+bottom2)),
4225 path._moveto(*bottom),
4226 graph._connect(*(bottom+bottom3))),
4227 *self.errorbarattrs)
4228 if top is not None and bottom is None:
4229 if center is not None:
4230 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
4231 top2 = graph._addpos(*(top+(self._errorsize, 0)))
4232 graph.stroke(path.path(path._moveto(*top1),
4233 graph._connect(*(top1+top2)),
4234 path._moveto(*top),
4235 graph._connect(*(top+center))),
4236 *self.errorbarattrs)
4237 else:
4238 top1 = graph._addpos(*(top+(-self._errorsize, 0)))
4239 top2 = graph._addpos(*(top+(self._errorsize, 0)))
4240 top3 = graph._addpos(*(top+(0, -self._errorsize)))
4241 graph.stroke(path.path(path._moveto(*top1),
4242 graph._connect(*(top1+top2)),
4243 path._moveto(*top),
4244 graph._connect(*(top+top3))),
4245 *self.errorbarattrs)
4246 if bottomleft is not None:
4247 if topleft is not None and bottomright is None:
4248 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
4249 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
4250 graph.stroke(path.path(path._moveto(*bottomleft1),
4251 graph._connect(*(bottomleft1+bottomleft)),
4252 graph._connect(*(bottomleft+topleft)),
4253 graph._connect(*(topleft+topleft1))),
4254 *self.errorbarattrs)
4255 elif bottomright is not None and topleft is None:
4256 bottomleft1 = graph._addpos(*(bottomleft+(0, self._errorsize)))
4257 bottomright1 = graph._addpos(*(bottomright+(0, self._errorsize)))
4258 graph.stroke(path.path(path._moveto(*bottomleft1),
4259 graph._connect(*(bottomleft1+bottomleft)),
4260 graph._connect(*(bottomleft+bottomright)),
4261 graph._connect(*(bottomright+bottomright1))),
4262 *self.errorbarattrs)
4263 elif bottomright is None and topleft is None:
4264 bottomleft1 = graph._addpos(*(bottomleft+(self._errorsize, 0)))
4265 bottomleft2 = graph._addpos(*(bottomleft+(0, self._errorsize)))
4266 graph.stroke(path.path(path._moveto(*bottomleft1),
4267 graph._connect(*(bottomleft1+bottomleft)),
4268 graph._connect(*(bottomleft+bottomleft2))),
4269 *self.errorbarattrs)
4270 if topright is not None:
4271 if bottomright is not None and topleft is None:
4272 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
4273 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
4274 graph.stroke(path.path(path._moveto(*topright1),
4275 graph._connect(*(topright1+topright)),
4276 graph._connect(*(topright+bottomright)),
4277 graph._connect(*(bottomright+bottomright1))),
4278 *self.errorbarattrs)
4279 elif topleft is not None and bottomright is None:
4280 topright1 = graph._addpos(*(topright+(0, -self._errorsize)))
4281 topleft1 = graph._addpos(*(topleft+(0, -self._errorsize)))
4282 graph.stroke(path.path(path._moveto(*topright1),
4283 graph._connect(*(topright1+topright)),
4284 graph._connect(*(topright+topleft)),
4285 graph._connect(*(topleft+topleft1))),
4286 *self.errorbarattrs)
4287 elif topleft is None and bottomright is None:
4288 topright1 = graph._addpos(*(topright+(-self._errorsize, 0)))
4289 topright2 = graph._addpos(*(topright+(0, -self._errorsize)))
4290 graph.stroke(path.path(path._moveto(*topright1),
4291 graph._connect(*(topright1+topright)),
4292 graph._connect(*(topright+topright2))),
4293 *self.errorbarattrs)
4294 if bottomright is not None and bottomleft is None and topright is None:
4295 bottomright1 = graph._addpos(*(bottomright+(-self._errorsize, 0)))
4296 bottomright2 = graph._addpos(*(bottomright+(0, self._errorsize)))
4297 graph.stroke(path.path(path._moveto(*bottomright1),
4298 graph._connect(*(bottomright1+bottomright)),
4299 graph._connect(*(bottomright+bottomright2))),
4300 *self.errorbarattrs)
4301 if topleft is not None and bottomleft is None and topright is None:
4302 topleft1 = graph._addpos(*(topleft+(self._errorsize, 0)))
4303 topleft2 = graph._addpos(*(topleft+(0, -self._errorsize)))
4304 graph.stroke(path.path(path._moveto(*topleft1),
4305 graph._connect(*(topleft1+topleft)),
4306 graph._connect(*(topleft+topleft2))),
4307 *self.errorbarattrs)
4308 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4309 graph.stroke(path.path(path._moveto(*bottomleft),
4310 graph._connect(*(bottomleft+bottomright)),
4311 graph._connect(*(bottomright+topright)),
4312 graph._connect(*(topright+topleft)),
4313 path.closepath()),
4314 *self.errorbarattrs)
4316 def _drawsymbol(self, canvas, x, y, point=None):
4317 canvas.draw(path.path(*self.symbol(self, x, y)), *self.symbolattrs)
4319 def drawsymbol(self, canvas, x, y, point=None):
4320 self._drawsymbol(canvas, unit.topt(x), unit.topt(y), point)
4322 def key(self, c, x, y, width, height):
4323 if self._symbolattrs is not None:
4324 self._drawsymbol(c, x + 0.5 * width, y + 0.5 * height)
4325 if self._lineattrs is not None:
4326 c.stroke(path._line(x, y + 0.5 * height, x + width, y + 0.5 * height), *self.lineattrs)
4328 def drawpoints(self, graph, points):
4329 xaxismin, xaxismax = self.xaxis.getrange()
4330 yaxismin, yaxismax = self.yaxis.getrange()
4331 self.size = unit.length(_getattr(self.size_str), default_type="v")
4332 self._size = unit.topt(self.size)
4333 self.symbol = _getattr(self._symbol)
4334 self.symbolattrs = _getattrs(helper.ensuresequence(self._symbolattrs))
4335 self.errorbarattrs = _getattrs(helper.ensuresequence(self._errorbarattrs))
4336 self._errorsize = self.errorscale * self._size
4337 self.errorsize = self.errorscale * self.size
4338 self.lineattrs = _getattrs(helper.ensuresequence(self._lineattrs))
4339 if self._lineattrs is not None:
4340 clipcanvas = graph.clipcanvas()
4341 lineels = []
4342 haserror = filter(None, (self.xmini, self.ymini, self.xmaxi, self.ymaxi,
4343 self.dxi, self.dyi, self.dxmini, self.dymini, self.dxmaxi, self.dymaxi)) is not None
4344 moveto = 1
4345 for point in points:
4346 drawsymbol = 1
4347 xmin, x, xmax = self.minmidmax(point, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4348 ymin, y, ymax = self.minmidmax(point, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4349 if x is not None and x < xaxismin: drawsymbol = 0
4350 elif x is not None and x > xaxismax: drawsymbol = 0
4351 elif y is not None and y < yaxismin: drawsymbol = 0
4352 elif y is not None and y > yaxismax: drawsymbol = 0
4353 # elif haserror: # TODO: correct clipcanvas handling
4354 # if xmin is not None and xmin < xaxismin: drawsymbol = 0
4355 # elif xmax is not None and xmax < xaxismin: drawsymbol = 0
4356 # elif xmax is not None and xmax > xaxismax: drawsymbol = 0
4357 # elif xmin is not None and xmin > xaxismax: drawsymbol = 0
4358 # elif ymin is not None and ymin < yaxismin: drawsymbol = 0
4359 # elif ymax is not None and ymax < yaxismin: drawsymbol = 0
4360 # elif ymax is not None and ymax > yaxismax: drawsymbol = 0
4361 # elif ymin is not None and ymin > yaxismax: drawsymbol = 0
4362 xpos=ypos=topleft=top=topright=left=center=right=bottomleft=bottom=bottomright=None
4363 if x is not None and y is not None:
4364 try:
4365 center = xpos, ypos = graph._pos(x, y, xaxis=self.xaxis, yaxis=self.yaxis)
4366 except (ValueError, OverflowError): # XXX: exceptions???
4367 pass
4368 if haserror:
4369 if y is not None:
4370 if xmin is not None: left = graph._pos(xmin, y, xaxis=self.xaxis, yaxis=self.yaxis)
4371 if xmax is not None: right = graph._pos(xmax, y, xaxis=self.xaxis, yaxis=self.yaxis)
4372 if x is not None:
4373 if ymax is not None: top = graph._pos(x, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4374 if ymin is not None: bottom = graph._pos(x, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4375 if x is None or y is None:
4376 if ymax is not None:
4377 if xmin is not None: topleft = graph._pos(xmin, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4378 if xmax is not None: topright = graph._pos(xmax, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4379 if ymin is not None:
4380 if xmin is not None: bottomleft = graph._pos(xmin, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4381 if xmax is not None: bottomright = graph._pos(xmax, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4382 if drawsymbol:
4383 if self._errorbarattrs is not None and haserror:
4384 self._drawerrorbar(graph, topleft, top, topright,
4385 left, center, right,
4386 bottomleft, bottom, bottomright, point)
4387 if self._symbolattrs is not None and xpos is not None and ypos is not None:
4388 self._drawsymbol(graph, xpos, ypos, point)
4389 if xpos is not None and ypos is not None:
4390 if moveto:
4391 lineels.append(path._moveto(xpos, ypos))
4392 moveto = 0
4393 else:
4394 lineels.append(path._lineto(xpos, ypos))
4395 else:
4396 moveto = 1
4397 self.path = path.path(*lineels)
4398 if self._lineattrs is not None:
4399 clipcanvas.stroke(self.path, *self.lineattrs)
4402 class changesymbol(changesequence): pass
4405 class _changesymbolcross(changesymbol):
4406 defaultsequence = (symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond)
4409 class _changesymbolplus(changesymbol):
4410 defaultsequence = (symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross)
4413 class _changesymbolsquare(changesymbol):
4414 defaultsequence = (symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus)
4417 class _changesymboltriangle(changesymbol):
4418 defaultsequence = (symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square)
4421 class _changesymbolcircle(changesymbol):
4422 defaultsequence = (symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle)
4425 class _changesymboldiamond(changesymbol):
4426 defaultsequence = (symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle)
4429 class _changesymbolsquaretwice(changesymbol):
4430 defaultsequence = (symbol.square, symbol.square, symbol.triangle, symbol.triangle,
4431 symbol.circle, symbol.circle, symbol.diamond, symbol.diamond)
4434 class _changesymboltriangletwice(changesymbol):
4435 defaultsequence = (symbol.triangle, symbol.triangle, symbol.circle, symbol.circle,
4436 symbol.diamond, symbol.diamond, symbol.square, symbol.square)
4439 class _changesymbolcircletwice(changesymbol):
4440 defaultsequence = (symbol.circle, symbol.circle, symbol.diamond, symbol.diamond,
4441 symbol.square, symbol.square, symbol.triangle, symbol.triangle)
4444 class _changesymboldiamondtwice(changesymbol):
4445 defaultsequence = (symbol.diamond, symbol.diamond, symbol.square, symbol.square,
4446 symbol.triangle, symbol.triangle, symbol.circle, symbol.circle)
4449 changesymbol.cross = _changesymbolcross
4450 changesymbol.plus = _changesymbolplus
4451 changesymbol.square = _changesymbolsquare
4452 changesymbol.triangle = _changesymboltriangle
4453 changesymbol.circle = _changesymbolcircle
4454 changesymbol.diamond = _changesymboldiamond
4455 changesymbol.squaretwice = _changesymbolsquaretwice
4456 changesymbol.triangletwice = _changesymboltriangletwice
4457 changesymbol.circletwice = _changesymbolcircletwice
4458 changesymbol.diamondtwice = _changesymboldiamondtwice
4461 class line(symbol):
4463 def __init__(self, lineattrs=helper.nodefault):
4464 if lineattrs is helper.nodefault:
4465 lineattrs = (changelinestyle(), style.linejoin.round)
4466 symbol.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
4469 class rect(symbol):
4471 def __init__(self, palette=color.palette.Gray):
4472 self.palette = palette
4473 self.colorindex = None
4474 symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
4476 def iterate(self):
4477 raise RuntimeError("style is not iterateable")
4479 def othercolumnkey(self, key, index):
4480 if key == "color":
4481 self.colorindex = index
4482 else:
4483 symbol.othercolumnkey(self, key, index)
4485 def _drawerrorbar(self, graph, topleft, top, topright,
4486 left, center, right,
4487 bottomleft, bottom, bottomright, point=None):
4488 color = point[self.colorindex]
4489 if color is not None:
4490 if color != self.lastcolor:
4491 self.rectclipcanvas.set(self.palette.getcolor(color))
4492 if bottom is not None and left is not None:
4493 bottomleft = left[0], bottom[1]
4494 if bottom is not None and right is not None:
4495 bottomright = right[0], bottom[1]
4496 if top is not None and right is not None:
4497 topright = right[0], top[1]
4498 if top is not None and left is not None:
4499 topleft = left[0], top[1]
4500 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4501 self.rectclipcanvas.fill(path.path(path._moveto(*bottomleft),
4502 graph._connect(*(bottomleft+bottomright)),
4503 graph._connect(*(bottomright+topright)),
4504 graph._connect(*(topright+topleft)),
4505 path.closepath()))
4507 def drawpoints(self, graph, points):
4508 if self.colorindex is None:
4509 raise RuntimeError("column 'color' not set")
4510 self.lastcolor = None
4511 self.rectclipcanvas = graph.clipcanvas()
4512 symbol.drawpoints(self, graph, points)
4514 def key(self, c, x, y, width, height):
4515 raise RuntimeError("style doesn't yet provide a key")
4518 class text(symbol):
4520 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=textmodule.halign.center, **args):
4521 self.textindex = None
4522 self.textdx_str = textdx
4523 self.textdy_str = textdy
4524 self._textattrs = textattrs
4525 symbol.__init__(self, **args)
4527 def iteratedict(self):
4528 result = symbol.iteratedict()
4529 result["textattrs"] = _iterateattr(self._textattrs)
4530 return result
4532 def iterate(self):
4533 return textsymbol(**self.iteratedict())
4535 def othercolumnkey(self, key, index):
4536 if key == "text":
4537 self.textindex = index
4538 else:
4539 symbol.othercolumnkey(self, key, index)
4541 def _drawsymbol(self, graph, x, y, point=None):
4542 symbol._drawsymbol(self, graph, x, y, point)
4543 if None not in (x, y, point[self.textindex]) and self._textattrs is not None:
4544 graph._text(x + self._textdx, y + self._textdy, str(point[self.textindex]), *helper.ensuresequence(self.textattrs))
4546 def drawpoints(self, graph, points):
4547 self.textdx = unit.length(_getattr(self.textdx_str), default_type="v")
4548 self.textdy = unit.length(_getattr(self.textdy_str), default_type="v")
4549 self._textdx = unit.topt(self.textdx)
4550 self._textdy = unit.topt(self.textdy)
4551 if self._textattrs is not None:
4552 self.textattrs = _getattr(self._textattrs)
4553 if self.textindex is None:
4554 raise RuntimeError("column 'text' not set")
4555 symbol.drawpoints(self, graph, points)
4557 def key(self, c, x, y, width, height):
4558 raise RuntimeError("style doesn't yet provide a key")
4561 class arrow(symbol):
4563 def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
4564 self.linelength_str = linelength
4565 self.arrowsize_str = arrowsize
4566 self.arrowattrs = arrowattrs
4567 self.arrowdict = arrowdict
4568 self.epsilon = epsilon
4569 self.sizeindex = self.angleindex = None
4570 symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
4572 def iterate(self):
4573 raise RuntimeError("style is not iterateable")
4575 def othercolumnkey(self, key, index):
4576 if key == "size":
4577 self.sizeindex = index
4578 elif key == "angle":
4579 self.angleindex = index
4580 else:
4581 symbol.othercolumnkey(self, key, index)
4583 def _drawsymbol(self, graph, x, y, point=None):
4584 if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
4585 if point[self.sizeindex] > self.epsilon:
4586 dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
4587 x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
4588 y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
4589 x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
4590 y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
4591 graph.stroke(path.line(x1, y1, x2, y2),
4592 deco.earrow(self.arrowsize*point[self.sizeindex],
4593 **self.arrowdict),
4594 *helper.ensuresequence(self.arrowattrs))
4596 def drawpoints(self, graph, points):
4597 self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
4598 self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
4599 self._arrowsize = unit.topt(self.arrowsize)
4600 self._linelength = unit.topt(self.linelength)
4601 if self.sizeindex is None:
4602 raise RuntimeError("column 'size' not set")
4603 if self.angleindex is None:
4604 raise RuntimeError("column 'angle' not set")
4605 symbol.drawpoints(self, graph, points)
4607 def key(self, c, x, y, width, height):
4608 raise RuntimeError("style doesn't yet provide a key")
4611 class _bariterator(changeattr):
4613 def attr(self, index):
4614 return index, self.counter
4617 class bar:
4619 def __init__(self, fromzero=1, stacked=0, skipmissing=1, xbar=0,
4620 barattrs=helper.nodefault, _usebariterator=helper.nodefault, _previousbar=None):
4621 self.fromzero = fromzero
4622 self.stacked = stacked
4623 self.skipmissing = skipmissing
4624 self.xbar = xbar
4625 if barattrs is helper.nodefault:
4626 self._barattrs = (deco.stroked(color.gray.black), changecolor.Rainbow())
4627 else:
4628 self._barattrs = barattrs
4629 if _usebariterator is helper.nodefault:
4630 self.bariterator = _bariterator()
4631 else:
4632 self.bariterator = _usebariterator
4633 self.previousbar = _previousbar
4635 def iteratedict(self):
4636 result = {}
4637 result["barattrs"] = _iterateattrs(self._barattrs)
4638 return result
4640 def iterate(self):
4641 return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
4642 _usebariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
4644 def setcolumns(self, graph, columns):
4645 def checkpattern(key, index, pattern, iskey, isindex):
4646 if key is not None:
4647 match = pattern.match(key)
4648 if match:
4649 if isindex is not None: raise ValueError("multiple key specification")
4650 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4651 key = None
4652 iskey = match.groups()[0]
4653 isindex = index
4654 return key, iskey, isindex
4656 xkey = ykey = None
4657 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4658 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4659 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4660 xi = yi = None
4661 for key, index in columns.items():
4662 key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
4663 key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
4664 if key is not None:
4665 self.othercolumnkey(key, index)
4666 if None in (xkey, ykey): raise ValueError("incomplete axis specification")
4667 if self.xbar:
4668 self.nkey, self.ni = ykey, yi
4669 self.vkey, self.vi = xkey, xi
4670 else:
4671 self.nkey, self.ni = xkey, xi
4672 self.vkey, self.vi = ykey, yi
4673 self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
4675 def getranges(self, points):
4676 index, count = _getattr(self.bariterator)
4677 if count != 1 and self.stacked != 1:
4678 if self.stacked > 1:
4679 index = divmod(index, self.stacked)[0]
4681 vmin = vmax = None
4682 for point in points:
4683 if not self.skipmissing:
4684 if count != 1 and self.stacked != 1:
4685 self.naxis.setname(point[self.ni], index)
4686 else:
4687 self.naxis.setname(point[self.ni])
4688 try:
4689 v = point[self.vi] + 0.0
4690 if vmin is None or v < vmin: vmin = v
4691 if vmax is None or v > vmax: vmax = v
4692 except (TypeError, ValueError):
4693 pass
4694 else:
4695 if self.skipmissing:
4696 if count != 1 and self.stacked != 1:
4697 self.naxis.setname(point[self.ni], index)
4698 else:
4699 self.naxis.setname(point[self.ni])
4700 if self.fromzero:
4701 if vmin > 0: vmin = 0
4702 if vmax < 0: vmax = 0
4703 return {self.vkey: (vmin, vmax)}
4705 def drawpoints(self, graph, points):
4706 index, count = _getattr(self.bariterator)
4707 dostacked = (self.stacked != 0 and
4708 (self.stacked == 1 or divmod(index, self.stacked)[1]) and
4709 (self.stacked != 1 or index))
4710 if self.stacked > 1:
4711 index = divmod(index, self.stacked)[0]
4712 vmin, vmax = self.vaxis.getrange()
4713 self.barattrs = _getattrs(helper.ensuresequence(self._barattrs))
4714 if self.stacked:
4715 self.stackedvalue = {}
4716 for point in points:
4717 try:
4718 n = point[self.ni]
4719 v = point[self.vi]
4720 if self.stacked:
4721 self.stackedvalue[n] = v
4722 if count != 1 and self.stacked != 1:
4723 minid = (n, index, 0)
4724 maxid = (n, index, 1)
4725 else:
4726 minid = (n, 0)
4727 maxid = (n, 1)
4728 if self.xbar:
4729 x1pos, y1pos = graph._pos(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
4730 x2pos, y2pos = graph._pos(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4731 else:
4732 x1pos, y1pos = graph._pos(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
4733 x2pos, y2pos = graph._pos(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
4734 if dostacked:
4735 if self.xbar:
4736 x3pos, y3pos = graph._pos(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
4737 x4pos, y4pos = graph._pos(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
4738 else:
4739 x3pos, y3pos = graph._pos(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4740 x4pos, y4pos = graph._pos(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4741 else:
4742 if self.fromzero:
4743 if self.xbar:
4744 x3pos, y3pos = graph._pos(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4745 x4pos, y4pos = graph._pos(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
4746 else:
4747 x3pos, y3pos = graph._pos(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4748 x4pos, y4pos = graph._pos(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4749 else:
4750 x3pos, y3pos = graph._tickpoint(maxid, axis=self.naxis)
4751 x4pos, y4pos = graph._tickpoint(minid, axis=self.naxis)
4752 if self.barattrs is not None:
4753 graph.fill(path.path(path._moveto(x1pos, y1pos),
4754 graph._connect(x1pos, y1pos, x2pos, y2pos),
4755 graph._connect(x2pos, y2pos, x3pos, y3pos),
4756 graph._connect(x3pos, y3pos, x4pos, y4pos),
4757 graph._connect(x4pos, y4pos, x1pos, y1pos), # no closepath (might not be straight)
4758 path.closepath()), *self.barattrs)
4759 except (TypeError, ValueError): pass
4761 def key(self, c, x, y, width, height):
4762 c.fill(path._rect(x, y, width, height), *self.barattrs)
4765 #class surface:
4767 # def setcolumns(self, graph, columns):
4768 # self.columns = columns
4770 # def getranges(self, points):
4771 # return {"x": (0, 10), "y": (0, 10), "z": (0, 1)}
4773 # def drawpoints(self, graph, points):
4774 # pass
4778 ################################################################################
4779 # data
4780 ################################################################################
4783 class data:
4785 defaultstyle = symbol
4787 def __init__(self, file, title=helper.nodefault, context={}, **columns):
4788 self.title = title
4789 if helper.isstring(file):
4790 self.data = datamodule.datafile(file)
4791 else:
4792 self.data = file
4793 if title is helper.nodefault:
4794 self.title = "(unknown)"
4795 else:
4796 self.title = title
4797 self.columns = {}
4798 for key, column in columns.items():
4799 try:
4800 self.columns[key] = self.data.getcolumnno(column)
4801 except datamodule.ColumnError:
4802 self.columns[key] = len(self.data.titles)
4803 self.data.addcolumn(column, context=context)
4805 def setstyle(self, graph, style):
4806 self.style = style
4807 self.style.setcolumns(graph, self.columns)
4809 def getranges(self):
4810 return self.style.getranges(self.data.data)
4812 def setranges(self, ranges):
4813 pass
4815 def draw(self, graph):
4816 self.style.drawpoints(graph, self.data.data)
4819 class function:
4821 defaultstyle = line
4823 def __init__(self, expression, title=helper.nodefault, min=None, max=None, points=100, parser=mathtree.parser(), context={}):
4824 if title is helper.nodefault:
4825 self.title = expression
4826 else:
4827 self.title = title
4828 self.min = min
4829 self.max = max
4830 self.points = points
4831 self.context = context
4832 self.result, expression = [x.strip() for x in expression.split("=")]
4833 self.mathtree = parser.parse(expression)
4834 self.variable = None
4835 self.evalranges = 0
4837 def setstyle(self, graph, style):
4838 for variable in self.mathtree.VarList():
4839 if variable in graph.axes.keys():
4840 if self.variable is None:
4841 self.variable = variable
4842 else:
4843 raise ValueError("multiple variables found")
4844 if self.variable is None:
4845 raise ValueError("no variable found")
4846 self.xaxis = graph.axes[self.variable]
4847 self.style = style
4848 self.style.setcolumns(graph, {self.variable: 0, self.result: 1})
4850 def getranges(self):
4851 if self.evalranges:
4852 return self.style.getranges(self.data)
4853 if None not in (self.min, self.max):
4854 return {self.variable: (self.min, self.max)}
4856 def setranges(self, ranges):
4857 if ranges.has_key(self.variable):
4858 min, max = ranges[self.variable]
4859 if self.min is not None: min = self.min
4860 if self.max is not None: max = self.max
4861 vmin = self.xaxis.convert(min)
4862 vmax = self.xaxis.convert(max)
4863 self.data = []
4864 for i in range(self.points):
4865 self.context[self.variable] = x = self.xaxis.invert(vmin + (vmax-vmin)*i / (self.points-1.0))
4866 try:
4867 y = self.mathtree.Calc(**self.context)
4868 except (ArithmeticError, ValueError):
4869 y = None
4870 self.data.append((x, y))
4871 self.evalranges = 1
4873 def draw(self, graph):
4874 self.style.drawpoints(graph, self.data)
4877 class paramfunction:
4879 defaultstyle = line
4881 def __init__(self, varname, min, max, expression, title=helper.nodefault, points=100, parser=mathtree.parser(), context={}):
4882 if title is helper.nodefault:
4883 self.title = expression
4884 else:
4885 self.title = title
4886 self.varname = varname
4887 self.min = min
4888 self.max = max
4889 self.points = points
4890 self.expression = {}
4891 self.mathtrees = {}
4892 varlist, expressionlist = expression.split("=")
4893 parsestr = mathtree.ParseStr(expressionlist)
4894 for key in varlist.split(","):
4895 key = key.strip()
4896 if self.mathtrees.has_key(key):
4897 raise ValueError("multiple assignment in tuple")
4898 try:
4899 self.mathtrees[key] = parser.ParseMathTree(parsestr)
4900 break
4901 except mathtree.CommaFoundMathTreeParseError, e:
4902 self.mathtrees[key] = e.MathTree
4903 else:
4904 raise ValueError("unpack tuple of wrong size")
4905 if len(varlist.split(",")) != len(self.mathtrees.keys()):
4906 raise ValueError("unpack tuple of wrong size")
4907 self.data = []
4908 for i in range(self.points):
4909 context[self.varname] = self.min + (self.max-self.min)*i / (self.points-1.0)
4910 line = []
4911 for key, tree in self.mathtrees.items():
4912 line.append(tree.Calc(**context))
4913 self.data.append(line)
4915 def setstyle(self, graph, style):
4916 self.style = style
4917 columns = {}
4918 for key, index in zip(self.mathtrees.keys(), xrange(sys.maxint)):
4919 columns[key] = index
4920 self.style.setcolumns(graph, columns)
4922 def getranges(self):
4923 return self.style.getranges(self.data)
4925 def setranges(self, ranges):
4926 pass
4928 def draw(self, graph):
4929 self.style.drawpoints(graph, self.data)