cleanups; all examples are working again
[PyX/mjg.git] / pyx / graph.py
blob5e01ae8c8c14539882ea37a155b89a1f913cc7b2
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]
67 def convert(self, value):
68 return self.y1 + self.dydx * (value - self.x1)
70 def invert(self, value):
71 return self.x1 + self.dxdy * (value - self.y1)
74 class _logmap:
75 "logarithmic mapping"
76 __implements__ = _Imap
78 def setbasepoints(self, basepoints):
79 self.dydx = ((basepoints[1][1] - basepoints[0][1]) /
80 float(math.log(basepoints[1][0]) - math.log(basepoints[0][0])))
81 self.dxdy = ((math.log(basepoints[1][0]) - math.log(basepoints[0][0])) /
82 float(basepoints[1][1] - basepoints[0][1]))
83 self.x1 = math.log(basepoints[0][0])
84 self.y1 = basepoints[0][1]
85 return self
87 def convert(self, value):
88 return self.y1 + self.dydx * (math.log(value) - self.x1)
90 def invert(self, value):
91 return math.exp(self.x1 + self.dxdy * (value - self.y1))
95 ################################################################################
96 # partitioner
97 # please note the nomenclature:
98 # - a partition is a list of tick instances; to reduce name clashes, a
99 # partition is called ticks
100 # - a partitioner is a class creating a single or several ticks
101 # - an axis has a part attribute where it stores a partitioner or/and some
102 # (manually set) ticks -> the part attribute is used to create the ticks
103 # in the axis finish method
104 ################################################################################
107 class frac:
108 """fraction class for rational arithmetics
109 the axis partitioning uses rational arithmetics (with infinite accuracy)
110 basically it contains self.enum and self.denom"""
112 def stringfrac(self, s):
113 "converts a string 0.123 into a frac"
114 expparts = s.split("e")
115 if len(expparts) > 2:
116 raise ValueError("multiple 'e' found in '%s'" % s)
117 commaparts = expparts[0].split(".")
118 if len(commaparts) > 2:
119 raise ValueError("multiple '.' found in '%s'" % expparts[0])
120 if len(commaparts) == 1:
121 commaparts = [commaparts[0], ""]
122 result = frac((1, 10l), power=len(commaparts[1]))
123 neg = len(commaparts[0]) and commaparts[0][0] == "-"
124 if neg:
125 commaparts[0] = commaparts[0][1:]
126 elif len(commaparts[0]) and commaparts[0][0] == "+":
127 commaparts[0] = commaparts[0][1:]
128 if len(commaparts[0]):
129 if not commaparts[0].isdigit():
130 raise ValueError("unrecognized characters in '%s'" % s)
131 x = long(commaparts[0])
132 else:
133 x = 0
134 if len(commaparts[1]):
135 if not commaparts[1].isdigit():
136 raise ValueError("unrecognized characters in '%s'" % s)
137 y = long(commaparts[1])
138 else:
139 y = 0
140 result.enum = x*result.denom+y
141 if neg:
142 result.enum = -result.enum
143 if len(expparts) == 2:
144 neg = expparts[1][0] == "-"
145 if neg:
146 expparts[1] = expparts[1][1:]
147 elif expparts[1][0] == "+":
148 expparts[1] = expparts[1][1:]
149 if not expparts[1].isdigit():
150 raise ValueError("unrecognized characters in '%s'" % s)
151 if neg:
152 result *= frac((1, 10l), power=long(expparts[1]))
153 else:
154 result *= frac((10, 1l), power=long(expparts[1]))
155 return result
157 def floatfrac(self, x, floatprecision):
158 "converts a float into a frac with finite resolution"
159 if helper.isinteger(floatprecision) and floatprecision < 0:
160 # this would be extremly vulnerable
161 raise RuntimeError("float resolution must be non-negative integer")
162 return self.stringfrac(("%%.%ig" % floatprecision) % x)
164 def __init__(self, x, power=None, floatprecision=10):
165 "for power!=None: frac=(enum/denom)**power"
166 if helper.isnumber(x):
167 value = self.floatfrac(x, floatprecision)
168 enum, denom = value.enum, value.denom
169 elif helper.isstring(x):
170 fraction = x.split("/")
171 if len(fraction) > 2:
172 raise ValueError("multiple '/' found in '%s'" % x)
173 value = self.stringfrac(fraction[0])
174 if len(fraction) == 2:
175 value2 = self.stringfrac(fraction[1])
176 value = value / value2
177 enum, denom = value.enum, value.denom
178 else:
179 try:
180 enum, denom = x
181 except (TypeError, AttributeError):
182 enum, denom = x.enum, x.denom
183 if not helper.isinteger(enum) or not helper.isinteger(denom): raise TypeError("integer type expected")
184 if not denom: raise ZeroDivisionError("zero denominator")
185 if power != None:
186 if not helper.isinteger(power): raise TypeError("integer type expected")
187 if power >= 0:
188 self.enum = long(enum) ** power
189 self.denom = long(denom) ** power
190 else:
191 self.enum = long(denom) ** (-power)
192 self.denom = long(enum) ** (-power)
193 else:
194 self.enum = enum
195 self.denom = denom
197 def __cmp__(self, other):
198 if other is None:
199 return 1
200 return cmp(self.enum * other.denom, other.enum * self.denom)
202 def __abs__(self):
203 return frac((abs(self.enum), abs(self.denom)))
205 def __mul__(self, other):
206 return frac((self.enum * other.enum, self.denom * other.denom))
208 def __div__(self, other):
209 return frac((self.enum * other.denom, self.denom * other.enum))
211 def __float__(self):
212 "caution: avoid final precision of floats"
213 return float(self.enum) / self.denom
215 def __str__(self):
216 return "%i/%i" % (self.enum, self.denom)
219 class tick(frac):
220 """tick class
221 a tick is a frac enhanced by
222 - self.ticklevel (0 = tick, 1 = subtick, etc.)
223 - self.labellevel (0 = label, 1 = sublabel, etc.)
224 - self.label (a string) and self.labelattrs (a list, defaults to [])
225 When ticklevel or labellevel is None, no tick or label is present at that value.
226 When label is None, it should be automatically created (and stored), once the
227 an axis painter needs it. Classes, which implement _Itexter do precisely that."""
229 def __init__(self, pos, ticklevel=0, labellevel=0, label=None, labelattrs=[], **kwargs):
230 """initializes the instance
231 - see class description for the parameter description
232 - **kwargs are passed to the frac constructor"""
233 frac.__init__(self, pos, **kwargs)
234 self.ticklevel = ticklevel
235 self.labellevel = labellevel
236 self.label = label
237 self.labelattrs = helper.ensurelist(labelattrs)[:]
239 def merge(self, other):
240 """merges two ticks together:
241 - the lower ticklevel/labellevel wins
242 - the label is *never* taken over from other
243 - the ticks should be at the same position (otherwise it doesn't make sense)
244 -> this is NOT checked"""
245 if self.ticklevel is None or (other.ticklevel is not None and other.ticklevel < self.ticklevel):
246 self.ticklevel = other.ticklevel
247 if self.labellevel is None or (other.labellevel is not None and other.labellevel < self.labellevel):
248 self.labellevel = other.labellevel
251 def _mergeticklists(list1, list2):
252 """helper function to merge tick lists
253 - return a merged list of ticks out of list1 and list2
254 - CAUTION: original lists have to be ordered
255 (the returned list is also ordered)
256 - CAUTION: original lists are modified and they share references to
257 the result list!"""
258 # TODO: improve this using bisect?!
259 if list1 is None: return list2
260 if list2 is None: return list1
261 i = 0
262 j = 0
263 try:
264 while 1: # we keep on going until we reach an index error
265 while list2[j] < list1[i]: # insert tick
266 list1.insert(i, list2[j])
267 i += 1
268 j += 1
269 if list2[j] == list1[i]: # merge tick
270 list1[i].merge(list2[j])
271 j += 1
272 i += 1
273 except IndexError:
274 if j < len(list2):
275 list1 += list2[j:]
276 return list1
279 def _mergelabels(ticks, labels):
280 """helper function to merge labels into ticks
281 - when labels is not None, the label of all ticks with
282 labellevel different from None are set
283 - labels need to be a list of lists of strings,
284 where the first list contain the strings to be
285 used as labels for the ticks with labellevel 0,
286 the second list for labellevel 1, etc.
287 - when the maximum labellevel is 0, just a list of
288 strings might be provided as the labels argument
289 - IndexError is raised, when a list length doesn't match"""
290 if helper.issequenceofsequences(labels):
291 for label, level in zip(labels, xrange(sys.maxint)):
292 usetext = helper.ensuresequence(label)
293 i = 0
294 for tick in ticks:
295 if tick.labellevel == level:
296 tick.label = usetext[i]
297 i += 1
298 if i != len(usetext):
299 raise IndexError("wrong list length of labels at level %i" % level)
300 elif labels is not None:
301 usetext = helper.ensuresequence(labels)
302 i = 0
303 for tick in ticks:
304 if tick.labellevel == 0:
305 tick.label = usetext[i]
306 i += 1
307 if i != len(usetext):
308 raise IndexError("wrong list length of labels")
311 class _Ipart:
312 """interface definition of a partition scheme
313 partition schemes are used to create a list of ticks"""
315 def defaultpart(self, min, max, extendmin, extendmax):
316 """create a partition
317 - returns an ordered list of ticks for the interval min to max
318 - the interval is given in float numbers, thus an appropriate
319 conversion to rational numbers has to be performed
320 - extendmin and extendmax are booleans (integers)
321 - when extendmin or extendmax is set, the ticks might
322 extend the min-max range towards lower and higher
323 ranges, respectively"""
325 def lesspart(self):
326 """create another partition which contains less ticks
327 - this method is called several times after a call of defaultpart
328 - returns an ordered list of ticks with less ticks compared to
329 the partition returned by defaultpart and by previous calls
330 of lesspart
331 - the creation of a partition with strictly *less* ticks
332 is not to be taken serious
333 - the method might return None, when no other appropriate
334 partition can be created"""
337 def morepart(self):
338 """create another partition which contains more ticks
339 see lesspart, but increase the number of ticks"""
342 class linpart:
343 """linear partition scheme
344 ticks and label distances are explicitly provided to the constructor"""
346 __implements__ = _Ipart
348 def __init__(self, tickdist=None, labeldist=None, labels=None, extendtick=0, extendlabel=None, epsilon=1e-10):
349 """configuration of the partition scheme
350 - tickdist and labeldist should be a list, where the first value
351 is the distance between ticks with ticklevel/labellevel 0,
352 the second list for ticklevel/labellevel 1, etc.;
353 a single entry is allowed without being a list
354 - tickdist and labeldist values are passed to the frac constructor
355 - when labeldist is None and tickdist is not None, the tick entries
356 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
357 - labels are applied to the resulting partition via the
358 mergelabels function (additional information available there)
359 - extendtick allows for the extension of the range given to the
360 defaultpart method to include the next tick with the specified
361 level (None turns off this feature); note, that this feature is
362 also disabled, when an axis prohibits its range extension by
363 the extendmin/extendmax variables given to the defaultpart method
364 - extendlabel is analogous to extendtick, but for labels
365 - epsilon allows for exceeding the axis range by this relative
366 value (relative to the axis range given to the defaultpart method)
367 without creating another tick specified by extendtick/extendlabel"""
368 if tickdist is None and labeldist is not None:
369 self.ticklist = (frac(helper.ensuresequence(labeldist)[0]),)
370 else:
371 self.ticklist = map(frac, helper.ensuresequence(tickdist))
372 if labeldist is None and tickdist is not None:
373 self.labellist = (frac(helper.ensuresequence(tickdist)[0]),)
374 else:
375 self.labellist = map(frac, helper.ensuresequence(labeldist))
376 self.labels = labels
377 self.extendtick = extendtick
378 self.extendlabel = extendlabel
379 self.epsilon = epsilon
381 def extendminmax(self, min, max, frac, extendmin, extendmax):
382 """return new min, max tuple extending the range min, max
383 - frac is the tick distance to be used
384 - extendmin and extendmax are booleans to allow for the extension"""
385 if extendmin:
386 min = float(frac) * math.floor(min / float(frac) + self.epsilon)
387 if extendmax:
388 max = float(frac) * math.ceil(max / float(frac) - self.epsilon)
389 return min, max
391 def getticks(self, min, max, frac, ticklevel=None, labellevel=None):
392 """return a list of equal spaced ticks
393 - the tick distance is frac, the ticklevel is set to ticklevel and
394 the labellevel is set to labellevel
395 - min, max is the range where ticks should be placed"""
396 imin = int(math.ceil(min / float(frac) - 0.5 * self.epsilon))
397 imax = int(math.floor(max / float(frac) + 0.5 * self.epsilon))
398 ticks = []
399 for i in range(imin, imax + 1):
400 ticks.append(tick((long(i) * frac.enum, frac.denom), ticklevel=ticklevel, labellevel=labellevel))
401 return ticks
403 def defaultpart(self, min, max, extendmin, extendmax):
404 if self.extendtick is not None and len(self.ticklist) > self.extendtick:
405 min, max = self.extendminmax(min, max, self.ticklist[self.extendtick], extendmin, extendmax)
406 if self.extendlabel is not None and len(self.labellist) > self.extendlabel:
407 min, max = self.extendminmax(min, max, self.labellist[self.extendlabel], extendmin, extendmax)
409 ticks = []
410 for i in range(len(self.ticklist)):
411 ticks = _mergeticklists(ticks, self.getticks(min, max, self.ticklist[i], ticklevel = i))
412 for i in range(len(self.labellist)):
413 ticks = _mergeticklists(ticks, self.getticks(min, max, self.labellist[i], labellevel = i))
415 _mergelabels(ticks, self.labels)
417 return ticks
419 def lesspart(self):
420 return None
422 def morepart(self):
423 return None
426 class autolinpart:
427 """automatic linear partition scheme
428 - possible tick distances are explicitly provided to the constructor
429 - tick distances are adjusted to the axis range by multiplication or division by 10"""
431 __implements__ = _Ipart
433 defaultvariants = ((frac((1, 1)), frac((1, 2))),
434 (frac((2, 1)), frac((1, 1))),
435 (frac((5, 2)), frac((5, 4))),
436 (frac((5, 1)), frac((5, 2))))
438 def __init__(self, variants=defaultvariants, extendtick=0, epsilon=1e-10):
439 """configuration of the partition scheme
440 - variants is a list of tickdist
441 - tickdist should be a list, where the first value
442 is the distance between ticks with ticklevel 0,
443 the second for ticklevel 1, etc.
444 - tickdist values are passed to the frac constructor
445 - labellevel is set to None except for those ticks in the partitions,
446 where ticklevel is zero. There labellevel is also set to zero.
447 - extendtick allows for the extension of the range given to the
448 defaultpart method to include the next tick with the specified
449 level (None turns off this feature); note, that this feature is
450 also disabled, when an axis prohibits its range extension by
451 the extendmin/extendmax variables given to the defaultpart method
452 - epsilon allows for exceeding the axis range by this relative
453 value (relative to the axis range given to the defaultpart method)
454 without creating another tick specified by extendtick"""
455 self.variants = variants
456 self.extendtick = extendtick
457 self.epsilon = epsilon
459 def defaultpart(self, min, max, extendmin, extendmax):
460 logmm = math.log(max - min) / math.log(10)
461 if logmm < 0: # correction for rounding towards zero of the int routine
462 base = frac((10L, 1), int(logmm - 1))
463 else:
464 base = frac((10L, 1), int(logmm))
465 ticks = map(frac, self.variants[0])
466 useticks = [tick * base for tick in ticks]
467 self.lesstickindex = self.moretickindex = 0
468 self.lessbase = frac((base.enum, base.denom))
469 self.morebase = frac((base.enum, base.denom))
470 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
471 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
472 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
474 def lesspart(self):
475 if self.lesstickindex < len(self.variants) - 1:
476 self.lesstickindex += 1
477 else:
478 self.lesstickindex = 0
479 self.lessbase.enum *= 10
480 ticks = map(frac, self.variants[self.lesstickindex])
481 useticks = [tick * self.lessbase for tick in ticks]
482 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
483 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
485 def morepart(self):
486 if self.moretickindex:
487 self.moretickindex -= 1
488 else:
489 self.moretickindex = len(self.variants) - 1
490 self.morebase.denom *= 10
491 ticks = map(frac, self.variants[self.moretickindex])
492 useticks = [tick * self.morebase for tick in ticks]
493 part = linpart(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
494 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
497 class preexp:
498 """storage class for the definition of logarithmic axes partitions
499 instances of this class define tick positions suitable for
500 logarithmic axes by the following instance variables:
501 - exp: integer, which defines multiplicator (usually 10)
502 - pres: list of tick positions (rational numbers, e.g. instances of frac)
503 possible positions are these tick positions and arbitrary divisions
504 and multiplications by the exp value"""
506 def __init__(self, pres, exp):
507 "create a preexp instance and store its pres and exp information"
508 self.pres = helper.ensuresequence(pres)
509 self.exp = exp
512 class logpart(linpart):
513 """logarithmic partition scheme
514 ticks and label positions are explicitly provided to the constructor"""
516 __implements__ = _Ipart
518 pre1exp5 = preexp(frac((1, 1)), 100000)
519 pre1exp4 = preexp(frac((1, 1)), 10000)
520 pre1exp3 = preexp(frac((1, 1)), 1000)
521 pre1exp2 = preexp(frac((1, 1)), 100)
522 pre1exp = preexp(frac((1, 1)), 10)
523 pre125exp = preexp((frac((1, 1)), frac((2, 1)), frac((5, 1))), 10)
524 pre1to9exp = preexp(map(lambda x: frac((x, 1)), range(1, 10)), 10)
525 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
527 def __init__(self, tickpos=None, labelpos=None, labels=None, extendtick=0, extendlabel=None, epsilon=1e-10):
528 """configuration of the partition scheme
529 - tickpos and labelpos should be a list, where the first entry
530 is a preexp instance describing ticks with ticklevel/labellevel 0,
531 the second is a preexp instance for ticklevel/labellevel 1, etc.;
532 a single entry is allowed without being a list
533 - when labelpos is None and tickpos is not None, the tick entries
534 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
535 - labels are applied to the resulting partition via the
536 mergetexts function (additional information available there)
537 - extendtick allows for the extension of the range given to the
538 defaultpart method to include the next tick with the specified
539 level (None turns off this feature); note, that this feature is
540 also disabled, when an axis prohibits its range extension by
541 the extendmin/extendmax variables given to the defaultpart method
542 - extendlabel is analogous to extendtick, but for labels
543 - epsilon allows for exceeding the axis range by this relative
544 logarithm value (relative to the logarithm axis range given
545 to the defaultpart method) without creating another tick
546 specified by extendtick/extendlabel"""
547 if tickpos is None and labels is not None:
548 self.ticklist = (helper.ensuresequence(labelpos)[0],)
549 else:
550 self.ticklist = helper.ensuresequence(tickpos)
552 if labelpos is None and tickpos is not None:
553 self.labellist = (helper.ensuresequence(tickpos)[0],)
554 else:
555 self.labellist = helper.ensuresequence(labelpos)
556 self.labels = labels
557 self.extendtick = extendtick
558 self.extendlabel = extendlabel
559 self.epsilon = epsilon
561 def extendminmax(self, min, max, preexp, extendmin, extendmax):
562 """return new min, max tuple extending the range min, max
563 preexp describes the allowed tick positions
564 extendmin and extendmax are booleans to allow for the extension"""
565 minpower = None
566 maxpower = None
567 for i in xrange(len(preexp.pres)):
568 imin = int(math.floor(math.log(min / float(preexp.pres[i])) /
569 math.log(preexp.exp) + self.epsilon)) + 1
570 imax = int(math.ceil(math.log(max / float(preexp.pres[i])) /
571 math.log(preexp.exp) - self.epsilon)) - 1
572 if minpower is None or imin < minpower:
573 minpower, minindex = imin, i
574 if maxpower is None or imax >= maxpower:
575 maxpower, maxindex = imax, i
576 if minindex:
577 minfrac = preexp.pres[minindex - 1]
578 else:
579 minfrac = preexp.pres[-1]
580 minpower -= 1
581 if maxindex != len(preexp.pres) - 1:
582 maxfrac = preexp.pres[maxindex + 1]
583 else:
584 maxfrac = preexp.pres[0]
585 maxpower += 1
586 if extendmin:
587 min = float(minfrac) * float(preexp.exp) ** minpower
588 if extendmax:
589 max = float(maxfrac) * float(preexp.exp) ** maxpower
590 return min, max
592 def getticks(self, min, max, preexp, ticklevel=None, labellevel=None):
593 """return a list of ticks
594 - preexp describes the allowed tick positions
595 - the ticklevel of the ticks is set to ticklevel and
596 the labellevel is set to labellevel
597 - min, max is the range where ticks should be placed"""
598 ticks = []
599 minimin = 0
600 maximax = 0
601 for f in preexp.pres:
602 fracticks = []
603 imin = int(math.ceil(math.log(min / float(f)) /
604 math.log(preexp.exp) - 0.5 * self.epsilon))
605 imax = int(math.floor(math.log(max / float(f)) /
606 math.log(preexp.exp) + 0.5 * self.epsilon))
607 for i in range(imin, imax + 1):
608 pos = f * frac((preexp.exp, 1), i)
609 fracticks.append(tick((pos.enum, pos.denom), ticklevel = ticklevel, labellevel = labellevel))
610 ticks = _mergeticklists(ticks, fracticks)
611 return ticks
614 class autologpart(logpart):
615 """automatic logarithmic partition scheme
616 possible tick positions are explicitly provided to the constructor"""
618 __implements__ = _Ipart
620 defaultvariants = (((logpart.pre1exp, # ticks
621 logpart.pre1to9exp), # subticks
622 (logpart.pre1exp, # labels
623 logpart.pre125exp)), # sublevels
625 ((logpart.pre1exp, # ticks
626 logpart.pre1to9exp), # subticks
627 None), # labels like ticks
629 ((logpart.pre1exp2, # ticks
630 logpart.pre1exp), # subticks
631 None), # labels like ticks
633 ((logpart.pre1exp3, # ticks
634 logpart.pre1exp), # subticks
635 None), # labels like ticks
637 ((logpart.pre1exp4, # ticks
638 logpart.pre1exp), # subticks
639 None), # labels like ticks
641 ((logpart.pre1exp5, # ticks
642 logpart.pre1exp), # subticks
643 None)) # labels like ticks
645 def __init__(self, variants=defaultvariants, extendtick=0, extendlabel=None, epsilon=1e-10):
646 """configuration of the partition scheme
647 - variants should be a list of pairs of lists of preexp
648 instances
649 - within each pair the first list contains preexp, where
650 the first preexp instance describes ticks positions with
651 ticklevel 0, the second preexp for ticklevel 1, etc.
652 - the second list within each pair describes the same as
653 before, but for labels
654 - within each pair: when the second entry (for the labels) is None
655 and the first entry (for the ticks) ticks is not None, the tick
656 entries for ticklevel 0 are used for labels and vice versa
657 (ticks<->labels)
658 - extendtick allows for the extension of the range given to the
659 defaultpart method to include the next tick with the specified
660 level (None turns off this feature); note, that this feature is
661 also disabled, when an axis prohibits its range extension by
662 the extendmin/extendmax variables given to the defaultpart method
663 - extendlabel is analogous to extendtick, but for labels
664 - epsilon allows for exceeding the axis range by this relative
665 logarithm value (relative to the logarithm axis range given
666 to the defaultpart method) without creating another tick
667 specified by extendtick/extendlabel"""
668 self.variants = variants
669 if len(variants) > 2:
670 self.variantsindex = divmod(len(variants), 2)[0]
671 else:
672 self.variantsindex = 0
673 self.extendtick = extendtick
674 self.extendlabel = extendlabel
675 self.epsilon = epsilon
677 def defaultpart(self, min, max, extendmin, extendmax):
678 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
679 self.morevariantsindex = self.variantsindex
680 self.lessvariantsindex = self.variantsindex
681 part = logpart(tickpos=self.variants[self.variantsindex][0], labelpos=self.variants[self.variantsindex][1],
682 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
683 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
685 def lesspart(self):
686 self.lessvariantsindex += 1
687 if self.lessvariantsindex < len(self.variants):
688 part = logpart(tickpos=self.variants[self.lessvariantsindex][0], labelpos=self.variants[self.lessvariantsindex][1],
689 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
690 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
692 def morepart(self):
693 self.morevariantsindex -= 1
694 if self.morevariantsindex >= 0:
695 part = logpart(tickpos=self.variants[self.morevariantsindex][0], labelpos=self.variants[self.morevariantsindex][1],
696 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
697 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
701 ################################################################################
702 # rater
703 # conseptional remarks:
704 # - raters are used to calculate a rating for a realization of something
705 # - here, a rating means a positive floating point value
706 # - ratings are used to order those realizations by their suitability (lower
707 # ratings are better)
708 # - a rating of None means not suitable at all (those realizations should be
709 # thrown out)
710 ################################################################################
713 class cuberater:
714 """a value rater
715 - a cube rater has an optimal value, where the rate becomes zero
716 - for a left (below the optimum) and a right value (above the optimum),
717 the rating is value is set to 1 (modified by an overall weight factor
718 for the rating)
719 - the analytic form of the rating is cubic for both, the left and
720 the right side of the rater, independently"""
722 # __implements__ = sole implementation
724 def __init__(self, opt, left=None, right=None, weight=1):
725 """initializes the rater
726 - by default, left is set to zero, right is set to 3*opt
727 - left should be smaller than opt, right should be bigger than opt
728 - weight should be positive and is a factor multiplicated to the rates"""
729 if left is None:
730 left = 0
731 if right is None:
732 right = 3*opt
733 self.opt = opt
734 self.left = left
735 self.right = right
736 self.weight = weight
738 def rate(self, value, density):
739 """returns a rating for a value
740 - the density lineary rescales the rater (the optimum etc.),
741 e.g. a value bigger than one increases the optimum (when it is
742 positive) and a value lower than one decreases the optimum (when
743 it is positive); the density itself should be positive"""
744 opt = self.opt * density
745 if value < opt:
746 other = self.left * density
747 elif value > opt:
748 other = self.right * density
749 else:
750 return 0
751 factor = (value - opt) / float(other - opt)
752 return self.weight * (factor ** 3)
755 class distancerater:
756 # TODO: update docstring
757 """a distance rater (rates a list of distances)
758 - the distance rater rates a list of distances by rating each independently
759 and returning the average rate
760 - there is an optimal value, where the rate becomes zero
761 - the analytic form is linary for values above the optimal value
762 (twice the optimal value has the rating one, three times the optimal
763 value has the rating two, etc.)
764 - the analytic form is reciprocal subtracting one for values below the
765 optimal value (halve the optimal value has the rating one, one third of
766 the optimal value has the rating two, etc.)"""
768 # __implements__ = sole implementation
770 def __init__(self, opt, weight=0.1):
771 """inititializes the rater
772 - opt is the optimal length (a visual PyX length)
773 - weight should be positive and is a factor multiplicated to the rates"""
774 self.opt_str = opt
775 self.weight = weight
777 def rate(self, distances, density):
778 """rate distances
779 - the distances are a list of positive floats in PostScript points
780 - the density lineary rescales the rater (the optimum etc.),
781 e.g. a value bigger than one increases the optimum (when it is
782 positive) and a value lower than one decreases the optimum (when
783 it is positive); the density itself should be positive"""
784 if len(distances):
785 opt = unit.topt(unit.length(self.opt_str, default_type="v")) / density
786 rate = 0
787 for distance in distances:
788 if distance < opt:
789 rate += self.weight * (opt / distance - 1)
790 else:
791 rate += self.weight * (distance / opt - 1)
792 return rate / float(len(distances))
795 class axisrater:
796 """a rater for ticks
797 - the rating of axes is splited into two separate parts:
798 - rating of the ticks in terms of the number of ticks, subticks,
799 labels, etc.
800 - rating of the label distances
801 - in the end, a rate for ticks is the sum of these rates
802 - it is useful to first just rate the number of ticks etc.
803 and selecting those partitions, where this fits well -> as soon
804 as an complete rate (the sum of both parts from the list above)
805 of a first ticks is below a rate of just the number of ticks,
806 subticks labels etc. of other ticks, those other ticks will never
807 be better than the first one -> we gain speed by minimizing the
808 number of ticks, where label distances have to be taken into account)
809 - both parts of the rating are shifted into instances of raters
810 defined above --- right now, there is not yet a strict interface
811 for this delegation (should be done as soon as it is needed)"""
813 # __implements__ = sole implementation
815 linticks = (cuberater(4), cuberater(10, weight=0.5), )
816 linlabels = (cuberater(4), )
817 logticks = (cuberater(5, right=20), cuberater(20, right=100, weight=0.5), )
818 loglabels = (cuberater(5, right=20), cuberater(5, left=-20, right=20, weight=0.5), )
819 stdrange = cuberater(1, weight=2)
820 stddistance = distancerater("1 cm")
822 def __init__(self, ticks=linticks, labels=linlabels, range=stdrange, distance=stddistance):
823 """initializes the axis rater
824 - ticks and labels are lists of instances of a value rater
825 - the first entry in ticks rate the number of ticks, the
826 second the number of subticks, etc.; when there are no
827 ticks of a level or there is not rater for a level, the
828 level is just ignored
829 - labels is analogous, but for labels
830 - within the rating, all ticks with a higher level are
831 considered as ticks for a given level
832 - range is a value rater instance, which rates the covering
833 of an axis range by the ticks (as a relative value of the
834 tick range vs. the axis range), ticks might cover less or
835 more than the axis range (for the standard automatic axis
836 partition schemes an extention of the axis range is normal
837 and should get some penalty)
838 - distance is an distance rater instance"""
839 self.ticks = ticks
840 self.labels = labels
841 self.range = range
842 self.distance = distance
844 def rateticks(self, axis, ticks, density):
845 """rates ticks by the number of ticks, subticks, labels etc.
846 - takes into account the number of ticks, subticks, labels
847 etc. and the coverage of the axis range by the ticks
848 - when there are no ticks of a level or there was not rater
849 given in the constructor for a level, the level is just
850 ignored
851 - the method returns the sum of the rating results divided
852 by the sum of the weights of the raters
853 - within the rating, all ticks with a higher level are
854 considered as ticks for a given level"""
855 maxticklevel = maxlabellevel = 0
856 for tick in ticks:
857 if tick.ticklevel >= maxticklevel:
858 maxticklevel = tick.ticklevel + 1
859 if tick.labellevel >= maxlabellevel:
860 maxlabellevel = tick.labellevel + 1
861 numticks = [0]*maxticklevel
862 numlabels = [0]*maxlabellevel
863 for tick in ticks:
864 if tick.ticklevel is not None:
865 for level in range(tick.ticklevel, maxticklevel):
866 numticks[level] += 1
867 if tick.labellevel is not None:
868 for level in range(tick.labellevel, maxlabellevel):
869 numlabels[level] += 1
870 rate = 0
871 weight = 0
872 for numtick, rater in zip(numticks, self.ticks):
873 rate += rater.rate(numtick, density)
874 weight += rater.weight
875 for numlabel, rater in zip(numlabels, self.labels):
876 rate += rater.rate(numlabel, density)
877 weight += rater.weight
878 return rate/weight
880 def raterange(self, tickrange, datarange):
881 """rate the range covered by the ticks compared to the range
882 of the data
883 - tickrange and datarange are the ranges covered by the ticks
884 and the data in graph coordinates
885 - usually, the datarange is 1 (ticks are calculated for a
886 given datarange)
887 - the ticks might cover less or more than the data range (for
888 the standard automatic axis partition schemes an extention
889 of the axis range is normal and should get some penalty)"""
890 return self.range.rate(tickrange, datarange)
892 def ratelayout(self, axiscanvas, density):
893 """rate distances of the labels in an axis canvas
894 - the distances should be collected as box distances of
895 subsequent labels
896 - the axiscanvas provides a labels attribute for easy
897 access to the labels whose distances have to be taken
898 into account
899 - the density is used within the distancerate instance"""
900 if len(axiscanvas.labels) > 1:
901 try:
902 distances = [axiscanvas.labels[i]._boxdistance(axiscanvas.labels[i+1]) for i in range(len(axiscanvas.labels) - 1)]
903 except box.BoxCrossError:
904 return None
905 return self.distance.rate(distances, density)
906 else:
907 return None
910 ################################################################################
911 # texter
912 # texter automatically create labels for tick instances
913 ################################################################################
916 class _Itexter:
918 def labels(self, ticks):
919 """fill the label attribute of ticks
920 - ticks is a list of instances of tick
921 - for each element of ticks the value of the attribute label is set to
922 a string appropriate to the attributes enum and denom of that tick
923 instance
924 - label attributes of the tick instances are just kept, whenever they
925 are not equal to None
926 - the method might extend the labelattrs attribute of the ticks"""
929 class rationaltexter:
930 "a texter creating rational labels (e.g. 'a/b' or even 'a \over b')"
931 # XXX: we use divmod here to be more expicit
933 __implements__ = _Itexter
935 def __init__(self, prefix="", infix="", suffix="",
936 enumprefix="", enuminfix="", enumsuffix="",
937 denomprefix="", denominfix="", denomsuffix="",
938 plus="", minus="-", minuspos=0, over=r"{{%s}\over{%s}}",
939 equaldenom=0, skip1=1, skipenum0=1, skipenum1=1, skipdenom1=1,
940 labelattrs=textmodule.mathmode):
941 r"""initializes the instance
942 - prefix, infix, and suffix (strings) are added at the begin,
943 immediately after the minus, and at the end of the label,
944 respectively
945 - prefixenum, infixenum, and suffixenum (strings) are added
946 to the labels enumerator correspondingly
947 - prefixdenom, infixdenom, and suffixdenom (strings) are added
948 to the labels denominator correspondingly
949 - plus or minus (string) is inserted for non-negative or negative numbers
950 - minuspos is an integer, which determines the position, where the
951 plus or minus sign has to be placed; the following values are allowed:
952 1 - writes the plus or minus in front of the enumerator
953 0 - writes the plus or minus in front of the hole fraction
954 -1 - writes the plus or minus in front of the denominator
955 - over (string) is taken as a format string generating the
956 fraction bar; it has to contain exactly two string insert
957 operators "%s" -- the first for the enumerator and the second
958 for the denominator; by far the most common examples are
959 r"{{%s}\over{%s}}" and "{{%s}/{%s}}"
960 - usually the enumerator and denominator are canceled; however,
961 when equaldenom is set, the least common multiple of all
962 denominators is used
963 - skip1 (boolean) just prints the prefix, the plus or minus,
964 the infix and the suffix, when the value is plus or minus one
965 and at least one of prefix, infix and the suffix is present
966 - skipenum0 (boolean) just prints a zero instead of
967 the hole fraction, when the enumerator is zero;
968 no prefixes, infixes, and suffixes are taken into account
969 - skipenum1 (boolean) just prints the enumprefix, the plus or minus,
970 the enuminfix and the enumsuffix, when the enum value is plus or minus one
971 and at least one of enumprefix, enuminfix and the enumsuffix is present
972 - skipdenom1 (boolean) just prints the enumerator instead of
973 the hole fraction, when the denominator is one and none of the parameters
974 denomprefix, denominfix and denomsuffix are set and minuspos is not -1 or the
975 fraction is positive
976 - labelattrs is a list of attributes for a texrunners text method;
977 a single is allowed without being a list; None is considered as
978 an empty list"""
979 self.prefix = prefix
980 self.infix = infix
981 self.suffix = suffix
982 self.enumprefix = enumprefix
983 self.enuminfix = enuminfix
984 self.enumsuffix = enumsuffix
985 self.denomprefix = denomprefix
986 self.denominfix = denominfix
987 self.denomsuffix = denomsuffix
988 self.plus = plus
989 self.minus = minus
990 self.minuspos = minuspos
991 self.over = over
992 self.equaldenom = equaldenom
993 self.skip1 = skip1
994 self.skipenum0 = skipenum0
995 self.skipenum1 = skipenum1
996 self.skipdenom1 = skipdenom1
997 self.labelattrs = helper.ensurelist(labelattrs)
999 def gcd(self, *n):
1000 """returns the greates common divisor of all elements in n
1001 - the elements of n must be non-negative integers
1002 - return None if the number of elements is zero
1003 - the greates common divisor is not affected when some
1004 of the elements are zero, but it becomes zero when
1005 all elements are zero"""
1006 if len(n) == 2:
1007 i, j = n
1008 if i < j:
1009 i, j = j, i
1010 while j > 0:
1011 i, (dummy, j) = j, divmod(i, j)
1012 return i
1013 if len(n):
1014 res = n[0]
1015 for i in n[1:]:
1016 res = self.gcd(res, i)
1017 return res
1019 def lcm(self, *n):
1020 """returns the least common multiple of all elements in n
1021 - the elements of n must be non-negative integers
1022 - return None if the number of elements is zero
1023 - the least common multiple is zero when some of the
1024 elements are zero"""
1025 if len(n):
1026 res = n[0]
1027 for i in n[1:]:
1028 res = divmod(res * i, self.gcd(res, i))[0]
1029 return res
1031 def labels(self, ticks):
1032 labeledticks = []
1033 for tick in ticks:
1034 if tick.label is None and tick.labellevel is not None:
1035 labeledticks.append(tick)
1036 tick.temp_fracenum = tick.enum
1037 tick.temp_fracdenom = tick.denom
1038 tick.temp_fracminus = 1
1039 if tick.temp_fracenum < 0:
1040 tick.temp_fracminus = -tick.temp_fracminus
1041 tick.temp_fracenum = -tick.temp_fracenum
1042 if tick.temp_fracdenom < 0:
1043 tick.temp_fracminus = -tick.temp_fracminus
1044 tick.temp_fracdenom = -tick.temp_fracdenom
1045 gcd = self.gcd(tick.temp_fracenum, tick.temp_fracdenom)
1046 (tick.temp_fracenum, dummy1), (tick.temp_fracdenom, dummy2) = divmod(tick.temp_fracenum, gcd), divmod(tick.temp_fracdenom, gcd)
1047 if self.equaldenom:
1048 equaldenom = self.lcm(*[tick.temp_fracdenom for tick in ticks if tick.label is None])
1049 if equaldenom is not None:
1050 for tick in labeledticks:
1051 factor, dummy = divmod(equaldenom, tick.temp_fracdenom)
1052 tick.temp_fracenum, tick.temp_fracdenom = factor * tick.temp_fracenum, factor * tick.temp_fracdenom
1053 for tick in labeledticks:
1054 fracminus = fracenumminus = fracdenomminus = ""
1055 if tick.temp_fracminus == -1:
1056 plusminus = self.minus
1057 else:
1058 plusminus = self.plus
1059 if self.minuspos == 0:
1060 fracminus = plusminus
1061 elif self.minuspos == 1:
1062 fracenumminus = plusminus
1063 elif self.minuspos == -1:
1064 fracdenomminus = plusminus
1065 else:
1066 raise RuntimeError("invalid minuspos")
1067 if self.skipenum0 and tick.temp_fracenum == 0:
1068 tick.label = "0"
1069 elif (self.skip1 and self.skipdenom1 and tick.temp_fracenum == 1 and tick.temp_fracdenom == 1 and
1070 (len(self.prefix) or len(self.infix) or len(self.suffix)) and
1071 not len(fracenumminus) and not len(self.enumprefix) and not len(self.enuminfix) and not len(self.enumsuffix) and
1072 not len(fracdenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix)):
1073 tick.label = "%s%s%s%s" % (self.prefix, fracminus, self.infix, self.suffix)
1074 else:
1075 if self.skipenum1 and tick.temp_fracenum == 1 and (len(self.enumprefix) or len(self.enuminfix) or len(self.enumsuffix)):
1076 tick.temp_fracenum = "%s%s%s%s" % (self.enumprefix, fracenumminus, self.enuminfix, self.enumsuffix)
1077 else:
1078 tick.temp_fracenum = "%s%s%s%i%s" % (self.enumprefix, fracenumminus, self.enuminfix, tick.temp_fracenum, self.enumsuffix)
1079 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):
1080 frac = tick.temp_fracenum
1081 else:
1082 tick.temp_fracdenom = "%s%s%s%i%s" % (self.denomprefix, fracdenomminus, self.denominfix, tick.temp_fracdenom, self.denomsuffix)
1083 frac = self.over % (tick.temp_fracenum, tick.temp_fracdenom)
1084 tick.label = "%s%s%s%s%s" % (self.prefix, fracminus, self.infix, frac, self.suffix)
1085 tick.labelattrs.extend(self.labelattrs)
1087 # del tick.temp_fracenum # we've inserted those temporary variables ... and do not care any longer about them
1088 # del tick.temp_fracdenom
1089 # del tick.temp_fracminus
1093 class decimaltexter:
1094 "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')"
1096 __implements__ = _Itexter
1098 def __init__(self, prefix="", infix="", suffix="", equalprecision=0,
1099 decimalsep=".", thousandsep="", thousandthpartsep="",
1100 plus="", minus="-", period=r"\overline{%s}", labelattrs=textmodule.mathmode):
1101 r"""initializes the instance
1102 - prefix, infix, and suffix (strings) are added at the begin,
1103 immediately after the minus, and at the end of the label,
1104 respectively
1105 - decimalsep, thousandsep, and thousandthpartsep (strings)
1106 are used as separators
1107 - plus or minus (string) is inserted for non-negative or negative numbers
1108 - period (string) is taken as a format string generating a period;
1109 it has to contain exactly one string insert operators "%s" for the
1110 period; usually it should be r"\overline{%s}"
1111 - labelattrs is a list of attributes for a texrunners text method;
1112 a single is allowed without being a list; None is considered as
1113 an empty list"""
1114 self.prefix = prefix
1115 self.infix = infix
1116 self.suffix = suffix
1117 self.equalprecision = equalprecision
1118 self.decimalsep = decimalsep
1119 self.thousandsep = thousandsep
1120 self.thousandthpartsep = thousandthpartsep
1121 self.plus = plus
1122 self.minus = minus
1123 self.period = period
1124 self.labelattrs = helper.ensurelist(labelattrs)
1126 def labels(self, ticks):
1127 labeledticks = []
1128 maxdecprecision = 0
1129 for tick in ticks:
1130 if tick.label is None and tick.labellevel is not None:
1131 labeledticks.append(tick)
1132 m, n = tick.enum, tick.denom
1133 if m < 0: m = -m
1134 if n < 0: n = -n
1135 whole, reminder = divmod(m, n)
1136 whole = str(whole)
1137 if len(self.thousandsep):
1138 l = len(whole)
1139 tick.label = ""
1140 for i in range(l):
1141 tick.label += whole[i]
1142 if not ((l-i-1) % 3) and l > i+1:
1143 tick.label += self.thousandsep
1144 else:
1145 tick.label = whole
1146 if reminder:
1147 tick.label += self.decimalsep
1148 oldreminders = []
1149 tick.temp_decprecision = 0
1150 while (reminder):
1151 tick.temp_decprecision += 1
1152 if reminder in oldreminders:
1153 tick.temp_decprecision = None
1154 periodstart = len(tick.label) - (len(oldreminders) - oldreminders.index(reminder))
1155 tick.label = tick.label[:periodstart] + self.period % tick.label[periodstart:]
1156 break
1157 oldreminders += [reminder]
1158 reminder *= 10
1159 whole, reminder = divmod(reminder, n)
1160 if not ((tick.temp_decprecision - 1) % 3) and tick.temp_decprecision > 1:
1161 tick.label += self.thousandthpartsep
1162 tick.label += str(whole)
1163 if maxdecprecision < tick.temp_decprecision:
1164 maxdecprecision = tick.temp_decprecision
1165 if self.equalprecision:
1166 for tick in labeledticks:
1167 if tick.temp_decprecision is not None:
1168 if tick.temp_decprecision == 0 and maxdecprecision > 0:
1169 tick.label += self.decimalsep
1170 for i in range(tick.temp_decprecision, maxdecprecision):
1171 if not ((i - 1) % 3) and i > 1:
1172 tick.label += self.thousandthpartsep
1173 tick.label += "0"
1174 for tick in labeledticks:
1175 if tick.enum * tick.denom < 0:
1176 plusminus = self.minus
1177 else:
1178 plusminus = self.plus
1179 tick.label = "%s%s%s%s%s" % (self.prefix, plusminus, self.infix, tick.label, self.suffix)
1180 tick.labelattrs.extend(self.labelattrs)
1182 # del tick.temp_decprecision # we've inserted this temporary variable ... and do not care any longer about it
1185 class exponentialtexter:
1186 "a texter creating labels with exponentials (e.g. '2\cdot10^5')"
1188 __implements__ = _Itexter
1190 def __init__(self, plus="", minus="-",
1191 mantissaexp=r"{{%s}\cdot10^{%s}}",
1192 nomantissaexp=r"{10^{%s}}",
1193 minusnomantissaexp=r"{-10^{%s}}",
1194 mantissamin=frac((1, 1)), mantissamax=frac((10, 1)),
1195 skipmantissa1=0, skipallmantissa1=1,
1196 mantissatexter=decimaltexter()):
1197 r"""initializes the instance
1198 - plus or minus (string) is inserted for non-negative or negative exponents
1199 - mantissaexp (string) is taken as a format string generating the exponent;
1200 it has to contain exactly two string insert operators "%s" --
1201 the first for the mantissa and the second for the exponent;
1202 examples are r"{{%s}\cdot10^{%s}}" and r"{{%s}{\rm e}^{%s}}"
1203 - nomantissaexp (string) is taken as a format string generating the exponent
1204 when the mantissa is one and should be skipped; it has to contain
1205 exactly one string insert operators "%s" for the exponent;
1206 an examples is r"{10^{%s}}"
1207 - minusnomantissaexp (string) is taken as a format string generating the exponent
1208 when the mantissa is minus one and should be skipped; it has to contain
1209 exactly one string insert operators "%s" for the exponent; might be set to None
1210 to disallow skipping of any mantissa minus one
1211 an examples is r"{-10^{%s}}"
1212 - mantissamin and mantissamax are the minimum and maximum of the mantissa;
1213 they are frac instances greater than zero and mantissamin < mantissamax;
1214 the sign of the tick is ignored here
1215 - skipmantissa1 (boolean) turns on skipping of any mantissa equals one
1216 (and minus when minusnomantissaexp is set)
1217 - skipallmantissa1 (boolean) as above, but all mantissas must be 1
1218 - mantissatexter is the texter for the mantissa"""
1219 self.plus = plus
1220 self.minus = minus
1221 self.mantissaexp = mantissaexp
1222 self.nomantissaexp = nomantissaexp
1223 self.minusnomantissaexp = minusnomantissaexp
1224 self.mantissamin = mantissamin
1225 self.mantissamax = mantissamax
1226 self.mantissamindivmax = self.mantissamin / self.mantissamax
1227 self.mantissamaxdivmin = self.mantissamax / self.mantissamin
1228 self.skipmantissa1 = skipmantissa1
1229 self.skipallmantissa1 = skipallmantissa1
1230 self.mantissatexter = mantissatexter
1232 def labels(self, ticks):
1233 labeledticks = []
1234 for tick in ticks:
1235 if tick.label is None and tick.labellevel is not None:
1236 tick.temp_orgenum, tick.temp_orgdenom = tick.enum, tick.denom
1237 labeledticks.append(tick)
1238 tick.temp_exp = 0
1239 if tick.enum:
1240 while abs(tick) >= self.mantissamax:
1241 tick.temp_exp += 1
1242 x = tick * self.mantissamindivmax
1243 tick.enum, tick.denom = x.enum, x.denom
1244 while abs(tick) < self.mantissamin:
1245 tick.temp_exp -= 1
1246 x = tick * self.mantissamaxdivmin
1247 tick.enum, tick.denom = x.enum, x.denom
1248 if tick.temp_exp < 0:
1249 tick.temp_exp = "%s%i" % (self.minus, -tick.temp_exp)
1250 else:
1251 tick.temp_exp = "%s%i" % (self.plus, tick.temp_exp)
1252 self.mantissatexter.labels(labeledticks)
1253 if self.minusnomantissaexp is not None:
1254 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if abs(tick.enum) == abs(tick.denom)])
1255 else:
1256 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if tick.enum == tick.denom])
1257 for tick in labeledticks:
1258 if (self.skipallmantissa1 and allmantissa1 or
1259 (self.skipmantissa1 and (tick.enum == tick.denom or
1260 (tick.enum == -tick.denom and self.minusnomantissaexp is not None)))):
1261 if tick.enum == tick.denom:
1262 tick.label = self.nomantissaexp % tick.temp_exp
1263 else:
1264 tick.label = self.minusnomantissaexp % tick.temp_exp
1265 else:
1266 tick.label = self.mantissaexp % (tick.label, tick.temp_exp)
1267 tick.enum, tick.denom = tick.temp_orgenum, tick.temp_orgdenom
1269 # del tick.temp_orgenum # we've inserted those temporary variables ... and do not care any longer about them
1270 # del tick.temp_orgdenom
1271 # del tick.temp_exp
1274 class defaulttexter:
1275 "a texter creating decimal or exponential labels"
1277 __implements__ = _Itexter
1279 def __init__(self, smallestdecimal=frac((1, 1000)),
1280 biggestdecimal=frac((9999, 1)),
1281 equaldecision=1,
1282 decimaltexter=decimaltexter(),
1283 exponentialtexter=exponentialtexter()):
1284 r"""initializes the instance
1285 - smallestdecimal and biggestdecimal are the smallest and
1286 biggest decimal values, where the decimaltexter should be used;
1287 they are frac instances; the sign of the tick is ignored here;
1288 a tick at zero is considered for the decimaltexter as well
1289 - equaldecision (boolean) uses decimaltexter or exponentialtexter
1290 globaly (set) or for each tick separately (unset)
1291 - decimaltexter and exponentialtexter are texters to be used"""
1292 self.smallestdecimal = smallestdecimal
1293 self.biggestdecimal = biggestdecimal
1294 self.equaldecision = equaldecision
1295 self.decimaltexter = decimaltexter
1296 self.exponentialtexter = exponentialtexter
1298 def labels(self, ticks):
1299 decticks = []
1300 expticks = []
1301 for tick in ticks:
1302 if tick.label is None and tick.labellevel is not None:
1303 if not tick.enum or (abs(tick) >= self.smallestdecimal and abs(tick) <= self.biggestdecimal):
1304 decticks.append(tick)
1305 else:
1306 expticks.append(tick)
1307 if self.equaldecision:
1308 if len(expticks):
1309 self.exponentialtexter.labels(ticks)
1310 else:
1311 self.decimaltexter.labels(ticks)
1312 else:
1313 for tick in decticks:
1314 self.decimaltexter.labels([tick])
1315 for tick in expticks:
1316 self.exponentialtexter.labels([tick])
1319 ################################################################################
1320 # axis painter
1321 ################################################################################
1324 class axiscanvas(canvas._canvas):
1325 """axis canvas
1326 - an axis canvas is a regular canvas returned by an
1327 axispainters painter method
1328 - it contains a PyX length extent to be used for the
1329 alignment of additional axes; the axis extent should
1330 be handled by the axispainters painter method; you may
1331 apprehend this as a size information comparable to a
1332 bounding box, which must be handled manually
1333 - it contains a list of textboxes called labels which are
1334 used to rate the distances between the labels if needed
1335 by the axis later on; the painter method has not only to
1336 insert the labels into this canvas, but should also fill
1337 this list, when a rating of the distances should be
1338 performed by the axis"""
1340 # __implements__ = sole implementation
1342 def __init__(self, *args, **kwargs):
1343 """initializes the instance
1344 - sets extent to zero
1345 - sets labels to an empty list"""
1346 canvas._canvas.__init__(self, *args, **kwargs)
1347 self.extent = 0
1348 self.labels = []
1351 class rotatetext:
1352 """create rotations accordingly to tick directions
1353 - upsidedown rotations are suppressed by rotating them by another 180 degree"""
1355 # __implements__ = sole implementation
1357 def __init__(self, direction, epsilon=1e-10):
1358 """initializes the instance
1359 - direction is an angle to be used relative to the tick direction
1360 - epsilon is the value by which 90 degrees can be exceeded before
1361 an 180 degree rotation is added"""
1362 self.direction = direction
1363 self.epsilon = epsilon
1365 def trafo(self, dx, dy):
1366 """returns a rotation transformation accordingly to the tick direction
1367 - dx and dy are the direction of the tick"""
1368 direction = self.direction - math.atan2(dy, dx) * 180 / math.pi
1369 while (direction > 90 + self.epsilon):
1370 direction -= 180
1371 while (direction < -90 - self.epsilon):
1372 direction += 180
1373 return trafomodule.rotate(direction)
1376 rotatetext.parallel = rotatetext(90)
1377 rotatetext.orthogonal = rotatetext(180)
1380 class _Iaxispainter:
1381 "class for painting axes"
1383 def paint(self, axispos, axis, ac=None):
1384 """paint the axis into an axiscanvas
1385 - returns the axiscanvas
1386 - when no axiscanvas is provided (the typical case), a new
1387 axiscanvas is created. however, when extending an painter
1388 by inheritance, painting on the same axiscanvas is supported
1389 by setting the axiscanvas attribute
1390 - axispos is an instance, which implements _Iaxispos to
1391 define the tick positions
1392 - the axis and should not be modified (we may
1393 add some temporary variables like axis.ticks[i].temp_xxx,
1394 which might be used just temporary) -- the idea is that
1395 all things can be used several times
1396 - also do not modify the instance (self) -- even this
1397 instance might be used several times; thus do not modify
1398 attributes like self.titleattrs etc. (use local copies)
1399 - the method might access some additional attributes from
1400 the axis, e.g. the axis title -- the axis painter should
1401 document this behavior and rely on the availability of
1402 those attributes -> it becomes a question of the proper
1403 usage of the combination of axis & axispainter
1404 - the axiscanvas is a axiscanvas instance and should be
1405 filled with ticks, labels, title, etc.; note that the
1406 extent and labels instance variables should be handled
1407 as documented in the axiscanvas"""
1410 class _Iaxispos:
1411 """interface definition of axis tick position methods
1412 - these methods are used for the postitioning of the ticks
1413 when painting an axis"""
1414 # TODO: should we add a local transformation (for label text etc?)
1415 # (this might replace tickdirection (and even tickposition?))
1417 def basepath(self, x1=None, x2=None):
1418 """return the basepath as a path
1419 - x1 is the start position; if not set, the basepath starts
1420 from the beginning of the axis, which might imply a
1421 value outside of the graph coordinate range [0; 1]
1422 - x2 is analogous to x1, but for the end position"""
1424 def vbasepath(self, v1=None, v2=None):
1425 """return the basepath as a path
1426 - like basepath, but for graph coordinates"""
1428 def gridpath(self, x):
1429 """return the gridpath as a path for a given position x
1430 - might return None when no gridpath is available"""
1432 def vgridpath(self, v):
1433 """return the gridpath as a path for a given position v
1434 in graph coordinates
1435 - might return None when no gridpath is available"""
1437 def tickpoint_pt(self, x):
1438 """return the position at the basepath as a tuple (x, y) in
1439 postscript points for the position x"""
1441 def tickpoint(self, x):
1442 """return the position at the basepath as a tuple (x, y) in
1443 in PyX length for the position x"""
1445 def vtickpoint_pt(self, v):
1446 "like tickpoint_pt, but for graph coordinates"
1448 def vtickpoint(self, v):
1449 "like tickpoint, but for graph coordinates"
1451 def tickdirection(self, x):
1452 """return the direction of a tick as a tuple (dx, dy) for the
1453 position x (the direction points towards the graph)"""
1455 def vtickdirection(self, v):
1456 """like tickposition, but for graph coordinates"""
1459 class _axispos:
1460 """implements those parts of _Iaxispos which can be build
1461 out of the axis convert method and other _Iaxispos methods
1462 - base _Iaxispos methods, which need to be implemented:
1463 - vbasepath
1464 - vgridpath
1465 - vtickpoint_pt
1466 - vtickdirection
1467 - other methods needed for _Iaxispos are build out of those
1468 listed above when this class is inherited"""
1470 def __init__(self, convert):
1471 """initializes the instance
1472 - convert is a convert method from an axis"""
1473 self.convert = convert
1475 def basepath(self, x1=None, x2=None):
1476 if x1 is None:
1477 if x2 is None:
1478 return self.vbasepath()
1479 else:
1480 return self.vbasepath(v2=self.convert(x2))
1481 else:
1482 if x2 is None:
1483 return self.vbasepath(v1=self.convert(x1))
1484 else:
1485 return self.vbasepath(v1=self.convert(x1), v2=self.convert(x2))
1487 def gridpath(self, x):
1488 return self.vgridpath(self.convert(x))
1490 def tickpoint_pt(self, x):
1491 return self.vtickpoint_pt(self.convert(x))
1493 def tickpoint(self, x):
1494 return self.vtickpoint(self.convert(x))
1496 def vtickpoint(self, v):
1497 return [unit.t_pt(x) for x in self.vtickpoint(v)]
1499 def tickdirection(self, x):
1500 return self.vtickdirection(self.convert(x))
1503 class pathaxispos(_axispos):
1504 """axis tick position methods along an arbitrary path"""
1506 __implements__ = _Iaxispos
1508 def __init__(self, p, convert, direction=1):
1509 self.path = p
1510 self.normpath = path.normpath(p)
1511 self.arclength = self.normpath.arclength(p)
1512 _axispos.__init__(self, convert)
1513 self.direction = direction
1515 def vbasepath(self, v1=None, v2=None):
1516 if v1 is None:
1517 if v2 is None:
1518 return self.path
1519 else:
1520 return self.normpath.split(self.normpath.lentopar(v2 * self.arclength))[0]
1521 else:
1522 if v2 is None:
1523 return self.normpath.split(self.normpath.lentopar(v1 * self.arclength))[1]
1524 else:
1525 return self.normpath.split(*self.normpath.lentopar([v1 * self.arclength, v2 * self.arclength]))[1]
1527 def vgridpath(self, v):
1528 return None
1530 def tickpoint_pt(self, v):
1531 # XXX: path._at missing!
1532 return [unit.topt(x) for x in self.normpath.at(self.normpath.lentopar(v * self.arclength))]
1534 def vtickdirection(self, v):
1535 t = self.normpath.tangent(self.normpath.lentopar(v * self.arclength))
1536 # XXX: path._begin and path._end missing!
1537 tbegin = [unit.topt[x] for x in t.begin()]
1538 tend = [unit.topt[x] for x in t.end()]
1539 dx = tend[0]-tbegin[0]
1540 dy = tend[1]-tbegin[1]
1541 norm = math.sqrt(dx*dx + dy*dy)
1542 if self.direction == 1:
1543 return dy/norm, -dx/norm
1544 elif self.direction == -1:
1545 return -dy/norm, dx/norm
1546 raise RuntimeError("unknown direction")
1549 class axistitlepainter:
1550 """class for painting an axis title
1551 - the axis must have a title attribute when using this painter;
1552 this title might be None"""
1554 __implements__ = _Iaxispainter
1556 def __init__(self, titledist="0.3 cm",
1557 titleattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1558 titledirection=rotatetext.parallel,
1559 titlepos=0.5,
1560 texrunner=textmodule.defaulttexrunner):
1561 """initialized the instance
1562 - titledist is a visual PyX length giving the distance
1563 of the title from the axis extent already there (a title might
1564 be added after labels or other things are plotted already)
1565 - labelattrs is a list of attributes for a texrunners text
1566 method; a single is allowed without being a list; None
1567 turns off the title
1568 - titledirection is an instance of rotatetext or None
1569 - titlepos is the position of the title in graph coordinates
1570 - texrunner is the texrunner to be used to create text
1571 (the texrunner is available for further use in derived
1572 classes as instance variable texrunner)"""
1573 self.titledist_str = titledist
1574 self.titleattrs = titleattrs
1575 self.titledirection = titledirection
1576 self.titlepos = titlepos
1577 self.texrunner = texrunner
1579 def paint(self, axispos, axis, ac=None):
1580 if ac is None:
1581 ac = axiscanvas()
1582 if axis.title is not None and self.titleattrs is not None:
1583 titledist = unit.length(self.titledist_str, default_type="v")
1584 x, y = axispos.vtickpoint_pt(self.titlepos)
1585 dx, dy = axispos.vtickdirection(self.titlepos)
1586 titleattrs = helper.ensurelist(self.titleattrs)
1587 if self.titledirection is not None:
1588 titleattrs.append(self.titledirection.trafo(dx, dy))
1589 title = self.texrunner.text_pt(x, y, axis.title, *titleattrs)
1590 ac.extent += titledist
1591 title.linealign(ac.extent, -dx, -dy)
1592 ac.extent += title.extent(dx, dy)
1593 ac.insert(title)
1594 return ac
1597 class axispainter(axistitlepainter):
1598 """class for painting the ticks and labels of an axis
1599 - the inherited titleaxispainter is used to paint the title of
1600 the axis
1601 - note that the type of the elements of ticks given as an argument
1602 of the paint method must be suitable for the tick position methods
1603 of the axis"""
1605 __implements__ = _Iaxispainter
1607 defaultticklengths = ["%0.5f cm" % (0.2*goldenmean**(-i)) for i in range(10)]
1609 def __init__(self, innerticklengths=defaultticklengths,
1610 outerticklengths=None,
1611 tickattrs=(),
1612 gridattrs=None,
1613 zeropathattrs=(),
1614 basepathattrs=style.linecap.square,
1615 labeldist="0.3 cm",
1616 labelattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1617 labeldirection=None,
1618 labelhequalize=0,
1619 labelvequalize=1,
1620 **kwargs):
1621 """initializes the instance
1622 - innerticklenths and outerticklengths are two lists of
1623 visual PyX lengths for ticks, subticks, etc. plotted inside
1624 and outside of the graph; when a single value is given, it
1625 is used for all tick levels; None turns off ticks inside or
1626 outside of the graph
1627 - tickattrs are a list of stroke attributes for the ticks;
1628 a single entry is allowed without being a list; None turns
1629 off ticks
1630 - gridattrs are a list of lists used as stroke
1631 attributes for ticks, subticks etc.; when a single list
1632 is given, it is used for ticks, subticks, etc.; a single
1633 entry is allowed without being a list; None turns off
1634 the grid
1635 - zeropathattrs are a list of stroke attributes for a grid
1636 line at axis value zero; a single entry is allowed without
1637 being a list; None turns off the zeropath
1638 - basepathattrs are a list of stroke attributes for a grid
1639 line at axis value zero; a single entry is allowed without
1640 being a list; None turns off the basepath
1641 - labeldist is a visual PyX length for the distance of the labels
1642 from the axis basepath
1643 - labelattrs is a list of attributes for a texrunners text
1644 method; a single entry is allowed without being a list;
1645 None turns off the labels
1646 - titledirection is an instance of rotatetext or None
1647 - labelhequalize and labelvequalize (booleans) perform an equal
1648 alignment for straight vertical and horizontal axes, respectively
1649 - futher keyword arguments are passed to axistitlepainter"""
1650 # TODO: access to axis.divisor -- document, remove, ... ???
1651 self.innerticklengths_str = innerticklengths
1652 self.outerticklengths_str = outerticklengths
1653 self.tickattrs = tickattrs
1654 self.gridattrs = gridattrs
1655 self.zeropathattrs = zeropathattrs
1656 self.basepathattrs = basepathattrs
1657 self.labeldist_str = labeldist
1658 self.labelattrs = labelattrs
1659 self.labeldirection = labeldirection
1660 self.labelhequalize = labelhequalize
1661 self.labelvequalize = labelvequalize
1662 axistitlepainter.__init__(self, **kwargs)
1664 def paint(self, axispos, axis, ac=None):
1665 if ac is None:
1666 ac = axiscanvas()
1667 else:
1668 raise RuntimeError("XXX") # XXX debug only
1669 labeldist = unit.length(self.labeldist_str, default_type="v")
1670 for tick in axis.ticks:
1671 tick.temp_v = axis.convert(float(tick) * axis.divisor)
1672 tick.temp_x, tick.temp_y = axispos.vtickpoint_pt(tick.temp_v)
1673 tick.temp_dx, tick.temp_dy = axispos.vtickdirection(tick.temp_v)
1675 # create & align tick.temp_labelbox
1676 for tick in axis.ticks:
1677 if tick.labellevel is not None:
1678 labelattrs = helper.getsequenceno(self.labelattrs, tick.labellevel)
1679 if labelattrs is not None:
1680 labelattrs = helper.ensurelist(labelattrs)[:]
1681 if self.labeldirection is not None:
1682 labelattrs.append(self.labeldirection.trafo(tick.temp_dx, tick.temp_dy))
1683 if tick.labelattrs is not None:
1684 labelattrs.extend(helper.ensurelist(tick.labelattrs))
1685 tick.temp_labelbox = self.texrunner.text_pt(tick.temp_x, tick.temp_y, tick.label, *labelattrs)
1686 if len(axis.ticks) > 1:
1687 equaldirection = 1
1688 for tick in axis.ticks[1:]:
1689 if tick.temp_dx != axis.ticks[0].temp_dx or tick.temp_dy != axis.ticks[0].temp_dy:
1690 equaldirection = 0
1691 else:
1692 equaldirection = 0
1693 if equaldirection and ((not axis.ticks[0].temp_dx and self.labelvequalize) or
1694 (not axis.ticks[0].temp_dy and self.labelhequalize)):
1695 if self.labelattrs is not None:
1696 box.linealignequal([tick.temp_labelbox for tick in axis.ticks if tick.labellevel is not None],
1697 labeldist, -axis.ticks[0].temp_dx, -axis.ticks[0].temp_dy)
1698 else:
1699 for tick in axis.ticks:
1700 if tick.labellevel is not None and self.labelattrs is not None:
1701 tick.temp_labelbox.linealign(labeldist, -tick.temp_dx, -tick.temp_dy)
1703 def mkv(arg):
1704 if helper.issequence(arg):
1705 return [unit.length(a, default_type="v") for a in arg]
1706 if arg is not None:
1707 return unit.length(arg, default_type="v")
1708 innerticklengths = mkv(self.innerticklengths_str)
1709 outerticklengths = mkv(self.outerticklengths_str)
1711 for tick in axis.ticks:
1712 if tick.ticklevel is not None:
1713 innerticklength = helper.getitemno(innerticklengths, tick.ticklevel)
1714 outerticklength = helper.getitemno(outerticklengths, tick.ticklevel)
1715 if innerticklength is not None or outerticklength is not None:
1716 if innerticklength is None:
1717 innerticklength = 0
1718 if outerticklength is None:
1719 outerticklength = 0
1720 tickattrs = helper.getsequenceno(self.tickattrs, tick.ticklevel)
1721 if tickattrs is not None:
1722 innerticklength_pt = unit.topt(innerticklength)
1723 outerticklength_pt = unit.topt(outerticklength)
1724 x1 = tick.temp_x + tick.temp_dx * innerticklength_pt
1725 y1 = tick.temp_y + tick.temp_dy * innerticklength_pt
1726 x2 = tick.temp_x - tick.temp_dx * outerticklength_pt
1727 y2 = tick.temp_y - tick.temp_dy * outerticklength_pt
1728 ac.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(tickattrs))
1729 if tick != frac((0, 1)) or self.zeropathattrs is None:
1730 gridattrs = helper.getsequenceno(self.gridattrs, tick.ticklevel)
1731 if gridattrs is not None:
1732 ac.stroke(axispos.vgridpath(tick.temp_v), *helper.ensuresequence(gridattrs))
1733 if outerticklength is not None and unit.topt(outerticklength) > unit.topt(ac.extent):
1734 ac.extent = outerticklength
1735 if outerticklength is not None and unit.topt(-innerticklength) > unit.topt(ac.extent):
1736 ac.extent = -innerticklength
1737 if tick.labellevel is not None and self.labelattrs is not None:
1738 ac.insert(tick.temp_labelbox)
1739 ac.labels.append(tick.temp_labelbox)
1740 extent = tick.temp_labelbox.extent(tick.temp_dx, tick.temp_dy) + labeldist
1741 if unit.topt(extent) > unit.topt(ac.extent):
1742 ac.extent = extent
1743 if self.basepathattrs is not None:
1744 ac.stroke(axispos.vbasepath(), *helper.ensuresequence(self.basepathattrs))
1745 if self.zeropathattrs is not None:
1746 if len(axis.ticks) and axis.ticks[0] * axis.ticks[-1] < frac((0, 1)):
1747 ac.stroke(axispos.gridpath(0), *helper.ensuresequence(self.zeropathattrs))
1749 # for tick in axis.ticks:
1750 # del tick.temp_v # we've inserted those temporary variables ... and do not care any longer about them
1751 # del tick.temp_x
1752 # del tick.temp_y
1753 # del tick.temp_dx
1754 # del tick.temp_dy
1755 # if tick.labellevel is not None and self.labelattrs is not None:
1756 # del tick.temp_labelbox
1758 axistitlepainter.paint(self, axispos, axis, ac=ac)
1760 return ac
1763 class linkaxispainter(axispainter):
1764 """class for painting a linked axis
1765 - the inherited axispainter is used to paint the axis
1766 - modifies some constructor defaults"""
1768 __implements__ = _Iaxispainter
1770 def __init__(self, zeropathattrs=None,
1771 labelattrs=None,
1772 titleattrs=None,
1773 **kwargs):
1774 """initializes the instance
1775 - the zeropathattrs default is set to None thus skipping the zeropath
1776 - the labelattrs default is set to None thus skipping the labels
1777 - the titleattrs default is set to None thus skipping the title
1778 - all keyword arguments are passed to axispainter"""
1779 axispainter.__init__(self, zeropathattrs=zeropathattrs,
1780 labelattrs=labelattrs,
1781 titleattrs=titleattrs,
1782 **kwargs)
1785 class subaxispos:
1786 """implementation of the _Iaxispos interface for a subaxis"""
1788 __implements__ = _Iaxispos
1790 def __init__(self, convert, baseaxispos, vmin, vmax, vminover, vmaxover):
1791 """initializes the instance
1792 - convert is the subaxis convert method
1793 - baseaxispos is the axispos instance of the base axis
1794 - vmin, vmax is the range covered by the subaxis in graph coordinates
1795 - vminover, vmaxover is the extended range of the subaxis including
1796 regions between several subaxes (for baseline drawing etc.)"""
1797 self.convert = convert
1798 self.baseaxispos = baseaxispos
1799 self.vmin = vmin
1800 self.vmax = vmax
1801 self.vminover = vminover
1802 self.vmaxover = vmaxover
1804 def basepath(self, x1=None, x2=None):
1805 if x1 is not None:
1806 v1 = self.vmin+self.convert(x1)*(self.vmax-self.vmin)
1807 else:
1808 v1 = self.vminover
1809 if x2 is not None:
1810 v2 = self.vmin+self.convert(x2)*(self.vmax-self.vmin)
1811 else:
1812 v2 = self.vmaxover
1813 return self.baseaxispos.vbasepath(v1, v2)
1815 def vbasepath(self, v1=None, v2=None):
1816 if v1 is not None:
1817 v1 = self.vmin+v1*(self.vmax-self.vmin)
1818 else:
1819 v1 = self.vminover
1820 if v2 is not None:
1821 v2 = self.vmin+v2*(self.vmax-self.vmin)
1822 else:
1823 v2 = self.vmaxover
1824 return self.baseaxispos.vbasepath(v1, v2)
1826 def gridpath(self, x):
1827 return self.baseaxispos.vgridpath(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1829 def vgridpath(self, v):
1830 return self.baseaxispos.vgridpath(self.vmin+v*(self.vmax-self.vmin))
1832 def tickpoint_pt(self, x, axis=None):
1833 return self.baseaxispos.vtickpoint_pt(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1835 def tickpoint(self, x, axis=None):
1836 return self.baseaxispos.vtickpoint(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1838 def vtickpoint_pt(self, v, axis=None):
1839 return self.baseaxispos.vtickpoint_pt(self.vmin+v*(self.vmax-self.vmin))
1841 def vtickpoint(self, v, axis=None):
1842 return self.baseaxispos.vtickpoint(self.vmin+v*(self.vmax-self.vmin))
1844 def tickdirection(self, x, axis=None):
1845 return self.baseaxispos.vtickdirection(self.vmin+self.convert(x)*(self.vmax-self.vmin))
1847 def vtickdirection(self, v, axis=None):
1848 return self.baseaxispos.vtickdirection(self.vmin+v*(self.vmax-self.vmin))
1851 class splitaxispainter(axistitlepainter):
1852 """class for painting a splitaxis
1853 - the inherited titleaxispainter is used to paint the title of
1854 the axis
1855 - the splitaxispainter access the subaxes attribute of the axis"""
1857 __implements__ = _Iaxispainter
1859 def __init__(self, breaklinesdist="0.05 cm",
1860 breaklineslength="0.5 cm",
1861 breaklinesangle=-60,
1862 breaklinesattrs=(),
1863 **args):
1864 """initializes the instance
1865 - breaklinesdist is a visual length of the distance between
1866 the two lines of the axis break
1867 - breaklineslength is a visual length of the length of the
1868 two lines of the axis break
1869 - breaklinesangle is the angle of the lines of the axis break
1870 - breaklinesattrs are a list of stroke attributes for the
1871 axis break lines; a single entry is allowed without being a
1872 list; None turns off the break lines
1873 - futher keyword arguments are passed to axistitlepainter"""
1874 self.breaklinesdist_str = breaklinesdist
1875 self.breaklineslength_str = breaklineslength
1876 self.breaklinesangle = breaklinesangle
1877 self.breaklinesattrs = breaklinesattrs
1878 axistitlepainter.__init__(self, **args)
1880 def paint(self, axispos, axis, ac=None):
1881 if ac is None:
1882 ac = axiscanvas()
1883 else:
1884 raise RuntimeError("XXX") # XXX debug only
1885 for subaxis in axis.subaxes:
1886 subac = subaxis.finish(subaxispos(subaxis.convert, axispos, subaxis.vmin, subaxis.vmax, subaxis.vminover, subaxis.vmaxover), axis)
1887 ac.insert(subac)
1888 if unit.topt(ac.extent) < unit.topt(subac.extent):
1889 ac.extent = subac.extent
1890 if self.breaklinesattrs is not None:
1891 self.breaklinesdist = unit.length(self.breaklinesdist_str, default_type="v")
1892 self.breaklineslength = unit.length(self.breaklineslength_str, default_type="v")
1893 self.sin = math.sin(self.breaklinesangle*math.pi/180.0)
1894 self.cos = math.cos(self.breaklinesangle*math.pi/180.0)
1895 breaklinesextent = (0.5*self.breaklinesdist*math.fabs(self.cos) +
1896 0.5*self.breaklineslength*math.fabs(self.sin))
1897 if unit.topt(ac.extent) < unit.topt(breaklinesextent):
1898 ac.extent = breaklinesextent
1899 for subaxis1, subaxis2 in zip(axis.subaxes[:-1], axis.subaxes[1:]):
1900 # use a tangent of the basepath (this is independent of the tickdirection)
1901 v = 0.5 * (subaxis1.vmax + subaxis2.vmin)
1902 p = path.normpath(axispos.vbasepath(v, None))
1903 breakline = p.tangent(0, self.breaklineslength)
1904 widthline = p.tangent(0, self.breaklinesdist).transformed(trafomodule.rotate(self.breaklinesangle+90, *breakline.begin()))
1905 tocenter = map(lambda x: 0.5*(x[0]-x[1]), zip(breakline.begin(), breakline.end()))
1906 towidth = map(lambda x: 0.5*(x[0]-x[1]), zip(widthline.begin(), widthline.end()))
1907 breakline = breakline.transformed(trafomodule.translate(*tocenter).rotated(self.breaklinesangle, *breakline.begin()))
1908 breakline1 = breakline.transformed(trafomodule.translate(*towidth))
1909 breakline2 = breakline.transformed(trafomodule.translate(-towidth[0], -towidth[1]))
1910 ac.fill(path.path(path.moveto(*breakline1.begin()),
1911 path.lineto(*breakline1.end()),
1912 path.lineto(*breakline2.end()),
1913 path.lineto(*breakline2.begin()),
1914 path.closepath()), color.gray.white)
1915 ac.stroke(breakline1, *helper.ensuresequence(self.breaklinesattrs))
1916 ac.stroke(breakline2, *helper.ensuresequence(self.breaklinesattrs))
1917 axistitlepainter.paint(self, axispos, axis, ac=ac)
1918 return ac
1921 class linksplitaxispainter(splitaxispainter):
1922 """class for painting a linked splitaxis
1923 - the inherited splitaxispainter is used to paint the axis
1924 - modifies some constructor defaults"""
1926 __implements__ = _Iaxispainter
1928 def __init__(self, titleattrs=None, **kwargs):
1929 """initializes the instance
1930 - the titleattrs default is set to None thus skipping the title
1931 - all keyword arguments are passed to splitaxispainter"""
1932 splitaxispainter.__init__(self, titleattrs=titleattrs, **kwargs)
1935 class baraxispainter(axistitlepainter):
1936 """class for painting a baraxis
1937 - the inherited titleaxispainter is used to paint the title of
1938 the axis
1939 - the baraxispainter access the multisubaxis, subaxis names, texts, and
1940 relsizes attributes"""
1942 __implements__ = _Iaxispainter
1944 def __init__(self, innerticklength=None,
1945 outerticklength=None,
1946 tickattrs=(),
1947 basepathattrs=style.linecap.square,
1948 namedist="0.3 cm",
1949 nameattrs=(textmodule.halign.center, textmodule.vshift.mathaxis),
1950 namedirection=None,
1951 namepos=0.5,
1952 namehequalize=0,
1953 namevequalize=1,
1954 **args):
1955 """initializes the instance
1956 - innerticklength and outerticklength are a visual length of
1957 the ticks to be plotted at the axis basepath to visually
1958 separate the bars; if neither innerticklength nor
1959 outerticklength are set, not ticks are plotted
1960 - breaklinesattrs are a list of stroke attributes for the
1961 axis tick; a single entry is allowed without being a
1962 list; None turns off the ticks
1963 - namedist is a visual PyX length for the distance of the bar
1964 names from the axis basepath
1965 - nameattrs is a list of attributes for a texrunners text
1966 method; a single entry is allowed without being a list;
1967 None turns off the names
1968 - namedirection is an instance of rotatetext or None
1969 - namehequalize and namevequalize (booleans) perform an equal
1970 alignment for straight vertical and horizontal axes, respectively
1971 - futher keyword arguments are passed to axistitlepainter"""
1972 self.innerticklength_str = innerticklength
1973 self.outerticklength_str = outerticklength
1974 self.tickattrs = tickattrs
1975 self.basepathattrs = basepathattrs
1976 self.namedist_str = namedist
1977 self.nameattrs = nameattrs
1978 self.namedirection = namedirection
1979 self.namepos = namepos
1980 self.namehequalize = namehequalize
1981 self.namevequalize = namevequalize
1982 axistitlepainter.__init__(self, **args)
1984 def paint(self, axispos, axis, ac=None):
1985 if ac is None:
1986 ac = axiscanvas()
1987 else:
1988 raise RuntimeError("XXX") # XXX debug only
1989 if axis.multisubaxis is not None:
1990 for subaxis in axis.subaxis:
1991 subac = subaxis.finish(subaxispos(subaxis.convert, axispos, subaxis.vmin, subaxis.vmax, None, None), axis)
1992 ac.insert(subac)
1993 if unit.topt(ac.extent) < unit.topt(subac.extent):
1994 ac.extent = subac.extent
1995 namepos = []
1996 for name in axis.names:
1997 v = axis.convert((name, self.namepos))
1998 x, y = axispos.vtickpoint_pt(v)
1999 dx, dy = axispos.vtickdirection(v)
2000 namepos.append((v, x, y, dx, dy))
2001 nameboxes = []
2002 if self.nameattrs is not None:
2003 for (v, x, y, dx, dy), name in zip(namepos, axis.names):
2004 nameattrs = helper.ensurelist(self.nameattrs)[:]
2005 if self.namedirection is not None:
2006 nameattrs.append(self.namedirection.trafo(tick.temp_dx, tick.temp_dy))
2007 if axis.texts.has_key(name):
2008 nameboxes.append(self.texrunner.text_pt(x, y, str(axis.texts[name]), *nameattrs))
2009 elif axis.texts.has_key(str(name)):
2010 nameboxes.append(self.texrunner.text_pt(x, y, str(axis.texts[str(name)]), *nameattrs))
2011 else:
2012 nameboxes.append(self.texrunner.text_pt(x, y, str(name), *nameattrs))
2013 labeldist = ac.extent + unit.length(self.namedist_str, default_type="v")
2014 if len(namepos) > 1:
2015 equaldirection = 1
2016 for np in namepos[1:]:
2017 if np[3] != namepos[0][3] or np[4] != namepos[0][4]:
2018 equaldirection = 0
2019 else:
2020 equaldirection = 0
2021 if equaldirection and ((not namepos[0][3] and self.namevequalize) or
2022 (not namepos[0][4] and self.namehequalize)):
2023 box.linealignequal(nameboxes, labeldist, -namepos[0][3], -namepos[0][4])
2024 else:
2025 for namebox, np in zip(nameboxes, namepos):
2026 namebox.linealign(labeldist, -np[3], -np[4])
2027 if self.innerticklength_str is not None:
2028 innerticklength = unit.length(self.innerticklength_str, default_type="v")
2029 innerticklength_pt = unit.topt(innerticklength)
2030 if self.tickattrs is not None and unit.topt(ac.extent) < -innerticklength_pt:
2031 ac.extent = -innerticklength
2032 elif self.outerticklength_str is not None:
2033 innerticklength = innerticklength_pt = 0
2034 if self.outerticklength_str is not None:
2035 outerticklength = unit.length(self.outerticklength_str, default_type="v")
2036 outerticklength_pt = unit.topt(outerticklength)
2037 if self.tickattrs is not None and unit.topt(ac.extent) < outerticklength_pt:
2038 ac.extent = outerticklength
2039 elif self.innerticklength_str is not None:
2040 outerticklength = outerticklength_pt = 0
2041 for (v, x, y, dx, dy), namebox in zip(namepos, nameboxes):
2042 newextent = namebox.extent(dx, dy) + labeldist
2043 if unit.topt(ac.extent) < unit.topt(newextent):
2044 ac.extent = newextent
2045 if self.tickattrs is not None and (self.innerticklength_str is not None or self.outerticklength_str is not None):
2046 for pos in axis.relsizes:
2047 if pos == axis.relsizes[0]:
2048 pos -= axis.firstdist
2049 elif pos != axis.relsizes[-1]:
2050 pos -= 0.5 * axis.dist
2051 v = pos / axis.relsizes[-1]
2052 x, y = axispos.vtickpoint_pt(v)
2053 dx, dy = axispos.vtickdirection(v)
2054 x1 = x + dx * innerticklength_pt
2055 y1 = y + dy * innerticklength_pt
2056 x2 = x - dx * outerticklength_pt
2057 y2 = y - dy * outerticklength_pt
2058 ac.stroke(path._line(x1, y1, x2, y2), *helper.ensuresequence(self.tickattrs))
2059 if self.basepathattrs is not None:
2060 p = axispos.vbasepath()
2061 if p is not None:
2062 ac.stroke(p, *helper.ensuresequence(self.basepathattrs))
2063 for namebox in nameboxes:
2064 ac.insert(namebox)
2065 axistitlepainter.paint(self, axispos, axis, ac=ac)
2066 return ac
2069 class linkbaraxispainter(baraxispainter):
2070 """class for painting a linked baraxis
2071 - the inherited baraxispainter is used to paint the axis
2072 - modifies some constructor defaults"""
2074 __implements__ = _Iaxispainter
2076 def __init__(self, nameattrs=None, titleattrs=None, **kwargs):
2077 """initializes the instance
2078 - the titleattrs default is set to None thus skipping the title
2079 - the nameattrs default is set to None thus skipping the names
2080 - all keyword arguments are passed to axispainter"""
2081 baraxispainter.__init__(self, nameattrs=nameattrs, titleattrs=titleattrs, **kwargs)
2084 ################################################################################
2085 # axes
2086 ################################################################################
2089 class _Iaxis:
2090 """interface definition of a axis
2091 - an axis should implement an convert and invert method like
2092 _Imap, but this is not part of this interface definition;
2093 one possibility is to mix-in a proper map class, but special
2094 purpose axes might do something else"""
2096 def convert(self, x):
2097 "convert a value into graph coordinates"
2099 def invert(self, v):
2100 "invert a graph coordinate to a axis value"
2102 def getrelsize(self):
2103 """returns the relative size (width) of the axis
2104 - for use in splitaxis, baraxis etc.
2105 - might return None if no size is available"""
2107 def setrange(self, min=None, max=None):
2108 """set the axis data range
2109 - the type of min and max must fit to the axis
2110 - min<max; the axis might be reversed, but this is
2111 expressed internally only (min<max all the time)
2112 - the axis might not apply the change of the range
2113 (e.g. when the axis range is fixed by the user),
2114 but usually the range is extended to contain the
2115 given range
2116 - for invalid parameters (e.g. negativ values at an
2117 logarithmic axis), an exception should be raised
2118 - a RuntimeError is raised, when setrange is called
2119 after the finish method"""
2121 def getrange(self):
2122 """return data range as a tuple (min, max)
2123 - min<max; the axis might be reversed, but this is
2124 expressed internally only
2125 - a RuntimeError exception is raised when no
2126 range is available"""
2128 def finish(self, axispos):
2129 """finishes the axis
2130 - axispos implements _Iaxispos
2131 - the finish method returns an axiscanvas, which should be
2132 insertable into the graph to finally paint the axis
2133 - any modification of the axis range should be disabled after
2134 the finish method was called"""
2135 # TODO: be more specific about exceptions
2137 def createlinkaxis(self, **kwargs):
2138 """create a link axis to the axis itself
2139 - typically, a link axis is a axis, which share almost
2140 all properties with the axis it is linked to
2141 - typically, the painter gets replaced by a painter
2142 which doesn't put any text to the axis"""
2145 class _axis:
2146 """base implementation a regular axis
2147 - typical usage is to mix-in a linmap or a logmap to
2148 complete the definition"""
2150 def __init__(self, min=None, max=None, reverse=0, divisor=1,
2151 title=None, painter=axispainter(), texter=defaulttexter(),
2152 density=1, maxworse=2):
2153 """initializes the instance
2154 - min and max fix the axis minimum and maximum, respectively;
2155 they are determined by the data to be plotted, when not fixed
2156 - reverse (boolean) reverses the minimum and the maximum of
2157 the axis
2158 - numerical divisor for the axis partitioning
2159 - title is a string containing the axis title
2160 - axispainter is the axis painter (should implement _Ipainter)
2161 - texter is the texter (should implement _Itexter)
2162 - density is a global parameter for the axis paritioning and
2163 axis rating; its default is 1, but the range 0.5 to 2.5 should
2164 be usefull to get less or more ticks by the automatic axis
2165 partitioning
2166 - maxworse is a number of trials with worse tick rating
2167 before giving up (usually it should not be needed to increase
2168 this value; increasing the number will slow down the automatic
2169 axis partitioning considerably)
2170 - note that some methods of this class want to access a
2171 part and a rating attribute of the instance; those
2172 attributes should be initialized by the constructors
2173 of derived classes"""
2174 if min is not None and max is not None and min > max:
2175 min, max, reverse = max, min, not reverse
2176 self.fixmin, self.fixmax, self.min, self.max, self.reverse = min is not None, max is not None, min, max, reverse
2177 self.divisor = divisor
2178 self.title = title
2179 self.painter = painter
2180 self.texter = texter
2181 self.density = density
2182 self.maxworse = maxworse
2183 self.canconvert = 0
2184 self.finishac = None
2185 self._setrange()
2187 def _setrange(self, min=None, max=None):
2188 if not self.fixmin and min is not None and (self.min is None or min < self.min):
2189 self.min = min
2190 if not self.fixmax and max is not None and (self.max is None or max > self.max):
2191 self.max = max
2192 if None not in (self.min, self.max):
2193 self.canconvert = 1
2194 if self.reverse:
2195 self.setbasepoints(((self.min, 1), (self.max, 0)))
2196 else:
2197 self.setbasepoints(((self.min, 0), (self.max, 1)))
2199 def _getrange(self):
2200 return self.min, self.max
2202 def _forcerange(self, range):
2203 self.min, self.max = range
2204 self._setrange()
2206 def setrange(self, min=None, max=None):
2207 if self.finishac is not None:
2208 raise RuntimeError("axis was already finished")
2209 self._setrange(min, max)
2211 def getrange(self):
2212 if self.min is not None and self.max is not None:
2213 return self.min, self.max
2215 def checkfraclist(self, fracs):
2216 "orders a list of fracs, equal entries are not allowed"
2217 if not len(fracs): return []
2218 sorted = list(fracs)
2219 sorted.sort()
2220 last = sorted[0]
2221 for item in sorted[1:]:
2222 if last == item:
2223 raise ValueError("duplicate entry found")
2224 last = item
2225 return sorted
2227 def finish(self, axispos, texrunner):
2228 if self.finishac is not None:
2229 return self.finishac
2231 min, max = self.getrange()
2232 parter = parterpos = None
2233 if self.part is not None:
2234 self.part = helper.ensurelist(self.part)
2235 for p, i in zip(self.part, xrange(sys.maxint)):
2236 if hasattr(p, "defaultpart"):
2237 if parter is not None:
2238 raise RuntimeError("only one partitioner allowed")
2239 parter = p
2240 parterpos = i
2241 if parter is None:
2242 self.ticks = self.checkfraclist(self.part)
2243 else:
2244 self.part[:parterpos] = self.checkfraclist(self.part[:parterpos])
2245 self.part[parterpos+1:] = self.checkfraclist(self.part[parterpos+1:])
2246 self.ticks = _mergeticklists(_mergeticklists(self.part[:parterpos],
2247 parter.defaultpart(min/self.divisor,
2248 max/self.divisor,
2249 not self.fixmin,
2250 not self.fixmax)),
2251 self.part[parterpos+1:])
2252 else:
2253 self.ticks = []
2254 # lesspart and morepart can be called after defaultpart;
2255 # this works although some axes may share their autoparting,
2256 # because the axes are processed sequentially
2257 first = 1
2258 if parter is not None:
2259 worse = 0
2260 nextpart = parter.lesspart
2261 while nextpart is not None:
2262 newticks = nextpart()
2263 if newticks is not None:
2264 if parterpos is not None:
2265 newticks = _mergeticklists(_mergeticklists(self.part[:parterpos], newticks), self.part[parterpos+1:])
2266 if first:
2267 bestrate = self.rater.rateticks(self, self.ticks, self.density)
2268 bestrate += self.rater.raterange(self.convert(float(self.ticks[-1])/self.divisor)-
2269 self.convert(float(self.ticks[0])/self.divisor), 1)
2270 variants = [[bestrate, self.ticks]]
2271 first = 0
2272 else:
2273 newrate = self.rater.rateticks(self, newticks, self.density)
2274 newrate += self.rater.raterange(self.convert(float(self.ticks[-1])/self.divisor)-
2275 self.convert(float(self.ticks[0])/self.divisor), 1)
2276 variants.append([newrate, newticks])
2277 if newrate < bestrate:
2278 bestrate = newrate
2279 worse = 0
2280 else:
2281 worse += 1
2282 else:
2283 worse += 1
2284 if worse == self.maxworse and nextpart == parter.lesspart:
2285 worse = 0
2286 nextpart = parter.morepart
2287 if worse == self.maxworse and nextpart == parter.morepart:
2288 nextpart = None
2289 if not first:
2290 variants.sort()
2291 if self.painter is not None:
2292 i = 0
2293 bestrate = None
2294 while i < len(variants) and (bestrate is None or variants[i][0] < bestrate):
2295 saverange = self._getrange()
2296 self.ticks = variants[i][1]
2297 if len(self.ticks):
2298 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2299 self.texter.labels(self.ticks)
2300 ac = self.painter.paint(axispos, self)
2301 ratelayout = self.rater.ratelayout(ac, self.density)
2302 if ratelayout is not None:
2303 variants[i][0] += ratelayout
2304 variants[i].append(ac)
2305 else:
2306 variants[i][0] = None
2307 if variants[i][0] is not None and (bestrate is None or variants[i][0] < bestrate):
2308 bestrate = variants[i][0]
2309 self._forcerange(saverange)
2310 i += 1
2311 if bestrate is None:
2312 raise RuntimeError("no valid axis partitioning found")
2313 variants = [variant for variant in variants[:i] if variant[0] is not None]
2314 variants.sort()
2315 self.ticks = variants[0][1]
2316 if len(self.ticks):
2317 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2318 ac = variants[0][2]
2319 else:
2320 if len(self.ticks):
2321 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2322 ac = axiscanvas()
2323 else:
2324 if len(self.ticks):
2325 self.setrange(float(self.ticks[0])*self.divisor, float(self.ticks[-1])*self.divisor)
2326 self.texter.labels(self.ticks)
2327 ac = self.painter.paint(axispos, self)
2328 self.finishac = ac
2329 return self.finishac
2331 def createlinkaxis(self, **args):
2332 return linkaxis(self, **args)
2335 class linaxis(_axis, _linmap):
2336 """implementation of a linear axis"""
2338 __implements__ = _Iaxis
2340 def __init__(self, part=autolinpart(), rater=axisrater(), **args):
2341 """initializes the instance
2342 - the part attribute contains a list of one partitioner
2343 (a partitioner implements _Ipart) or/and some (manually
2344 set) ticks (implementing _Itick); a single entry might
2345 be passed without wrapping it into a list; the partitioner
2346 and the tick instances must fit to the type of the axis
2347 (e.g. they should be valid parameters to the axis convert
2348 method); the ticks and the partitioner results are mixed
2349 by _mergeticklists
2350 - the rater implements _Irater and is used to rate different
2351 tick lists created by the partitioner (after merging with
2352 manully set ticks)
2353 - futher keyword arguments are passed to _axis"""
2354 _axis.__init__(self, **args)
2355 if self.fixmin and self.fixmax:
2356 self.relsize = self.max - self.min
2357 self.part = part
2358 self.rater = rater
2361 class logaxis(_axis, _logmap):
2362 """implementation of a logarithmic axis"""
2364 __implements__ = _Iaxis
2366 def __init__(self, part=autologpart(), rater=axisrater(ticks=axisrater.logticks, labels=axisrater.loglabels), **args):
2367 """initializes the instance
2368 - the part attribute contains a list of one partitioner
2369 (a partitioner implements _Ipart) or/and some (manually
2370 set) ticks (implementing _Itick); a single entry might
2371 be passed without wrapping it into a list; the partitioner
2372 and the tick instances must fit to the type of the axis
2373 (e.g. they should be valid parameters to the axis convert
2374 method); the ticks and the partitioner results are mixed
2375 by _mergeticklists
2376 - the rater implements _Irater and is used to rate different
2377 tick lists created by the partitioner (after merging with
2378 manully set ticks)
2379 - futher keyword arguments are passed to _axis"""
2380 _axis.__init__(self, **args)
2381 if self.fixmin and self.fixmax:
2382 self.relsize = math.log(self.max) - math.log(self.min)
2383 self.part = part
2384 self.rater = rater
2387 class linkaxis:
2388 """a axis linked to an already existing regular axis
2389 - almost all properties of the axis are "copied" from the
2390 axis this axis is linked to
2391 - usually, linked axis are used to create an axis to an
2392 existing axis with different painting properties; linked
2393 axis can be used to plot an axis twice at the opposite
2394 sides of a graphxy or even to share an axis between
2395 different graphs!"""
2397 __implements__ = _Iaxis
2399 def __init__(self, linkedaxis, painter=linkaxispainter()):
2400 """initializes the instance
2401 - it gets a axis this linkaxis is linked to
2402 - it gets a painter to be used for this linked axis"""
2403 self.linkedaxis = linkedaxis
2404 self.painter = painter
2405 self.finishac = None
2407 def __getattr__(self, attr):
2408 """access to unkown attributes are handed over to the
2409 axis this linkaxis is linked to"""
2410 return getattr(self.linkedaxis, attr)
2412 def finish(self, axispos, texrunner):
2413 """finishes the axis
2414 - instead of performing the hole finish process
2415 (paritioning, rating, etc.) just a painter call
2416 is performed"""
2417 if self.finishac is None:
2418 self.linkedaxis.finish(axispos, texrunner)
2419 self.finishac = self.painter.paint(axispos, self)
2420 return self.finishac
2423 class splitaxis:
2424 """implementation of a split axis
2425 - a split axis contains several (sub-)axes with
2426 non-overlapping data ranges -- between these subaxes
2427 the axis is "splitted"
2428 - (just to get sure: a splitaxis can contain other
2429 splitaxes as its subaxes)
2430 - a splitaxis implements the _Iaxispos for its subaxes
2431 by inheritance from _subaxispos"""
2433 __implements__ = _Iaxis, _Iaxispos
2435 def __init__(self, subaxes, splitlist=0.5, splitdist=0.1, relsizesplitdist=1,
2436 title=None, painter=splitaxispainter()):
2437 """initializes the instance
2438 - subaxes is a list of subaxes
2439 - splitlist is a list of graph coordinates, where the splitting
2440 of the main axis should be performed; a single entry (splitting
2441 two axes) doesn't need to be wrapped into a list; if the list
2442 isn't long enough for the subaxes, missing entries are considered
2443 to be None;
2444 - splitdist is the size of the splitting in graph coordinates, when
2445 the associated splitlist entry is not None
2446 - relsizesplitdist: a None entry in splitlist means, that the
2447 position of the splitting should be calculated out of the
2448 relsize values of conrtibuting subaxes (the size of the
2449 splitting is relsizesplitdist in values of the relsize values
2450 of the axes)
2451 - title is the title of the axis as a string
2452 - painter is the painter of the axis; it should be specialized to
2453 the splitaxis
2454 - the relsize of the splitaxis is the sum of the relsizes of the
2455 subaxes including the relsizesplitdist"""
2456 self.subaxes = subaxes
2457 self.painter = painter
2458 self.title = title
2459 self.splitlist = helper.ensurelist(splitlist)
2460 for subaxis in self.subaxes:
2461 subaxis.vmin = None
2462 subaxis.vmax = None
2463 self.subaxes[0].vmin = 0
2464 self.subaxes[0].vminover = None
2465 self.subaxes[-1].vmax = 1
2466 self.subaxes[-1].vmaxover = None
2467 for i in xrange(len(self.splitlist)):
2468 if self.splitlist[i] is not None:
2469 self.subaxes[i].vmax = self.splitlist[i] - 0.5*splitdist
2470 self.subaxes[i].vmaxover = self.splitlist[i]
2471 self.subaxes[i+1].vmin = self.splitlist[i] + 0.5*splitdist
2472 self.subaxes[i+1].vminover = self.splitlist[i]
2473 i = 0
2474 while i < len(self.subaxes):
2475 if self.subaxes[i].vmax is None:
2476 j = relsize = relsize2 = 0
2477 while self.subaxes[i + j].vmax is None:
2478 relsize += self.subaxes[i + j].relsize + relsizesplitdist
2479 j += 1
2480 relsize += self.subaxes[i + j].relsize
2481 vleft = self.subaxes[i].vmin
2482 vright = self.subaxes[i + j].vmax
2483 for k in range(i, i + j):
2484 relsize2 += self.subaxes[k].relsize
2485 self.subaxes[k].vmax = vleft + (vright - vleft) * relsize2 / float(relsize)
2486 relsize2 += 0.5 * relsizesplitdist
2487 self.subaxes[k].vmaxover = self.subaxes[k + 1].vminover = vleft + (vright - vleft) * relsize2 / float(relsize)
2488 relsize2 += 0.5 * relsizesplitdist
2489 self.subaxes[k+1].vmin = vleft + (vright - vleft) * relsize2 / float(relsize)
2490 if i == 0 and i + j + 1 == len(self.subaxes):
2491 self.relsize = relsize
2492 i += j + 1
2493 else:
2494 i += 1
2496 self.fixmin = self.subaxes[0].fixmin
2497 if self.fixmin:
2498 self.min = self.subaxes[0].min
2499 self.fixmax = self.subaxes[-1].fixmax
2500 if self.fixmax:
2501 self.max = self.subaxes[-1].max
2503 self.finishac = None
2505 def getrange(self):
2506 min = self.subaxes[0].getrange()
2507 max = self.subaxes[-1].getrange()
2508 try:
2509 return min[0], max[1]
2510 except TypeError:
2511 return None
2513 def setrange(self, min, max):
2514 self.subaxes[0].setrange(min, None)
2515 self.subaxes[-1].setrange(None, max)
2517 def convert(self, value):
2518 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2519 if value < self.subaxes[0].max:
2520 return self.subaxes[0].vmin + self.subaxes[0].convert(value)*(self.subaxes[0].vmax-self.subaxes[0].vmin)
2521 for axis in self.subaxes[1:-1]:
2522 if value > axis.min and value < axis.max:
2523 return axis.vmin + axis.convert(value)*(axis.vmax-axis.vmin)
2524 if value > self.subaxes[-1].min:
2525 return self.subaxes[-1].vmin + self.subaxes[-1].convert(value)*(self.subaxes[-1].vmax-self.subaxes[-1].vmin)
2526 raise ValueError("value couldn't be assigned to a split region")
2528 def finish(self, axispos, texrunner):
2529 if self.finishac is None:
2530 self.finishac = self.painter.paint(axispos, self)
2531 return self.finishac
2533 def createlinkaxis(self, **args):
2534 return linksplitaxis(self, **args)
2537 class linksplitaxis(linkaxis):
2538 """a splitaxis linked to an already existing splitaxis
2539 - inherits the access to a linked axis -- as before,
2540 basically only the painter is replaced
2541 - it takes care of the creation of linked axes of
2542 the subaxes"""
2544 __implements__ = _Iaxis
2546 def __init__(self, linkedaxis, painter=linksplitaxispainter(), subaxispainter=None):
2547 """initializes the instance
2548 - it gets a axis this linkaxis is linked to
2549 - it gets a painter to be used for this linked axis
2550 - it gets a list of painters to be used for the linkaxes
2551 of the subaxes; if None, the createlinkaxis of the subaxes
2552 are called without a painter parameter; if it is not a
2553 list, the subaxispainter is passed as the painter
2554 parameter to all createlinkaxis of the subaxes"""
2555 linkaxis.__init__(self, linkedaxis, painter=painter)
2556 if subaxispainter is not None:
2557 if helper.issequence(subaxispainter):
2558 if len(linkedaxis.subaxes) != len(subaxispainter):
2559 raise RuntimeError("subaxes and subaxispainter lengths do not fit")
2560 self.subaxes = [a.createlinkaxis(painter=p) for a, p in zip(linkedaxis.subaxes, subaxispainter)]
2561 else:
2562 self.subaxes = [a.createlinkaxis(painter=subaxispainter) for a in linkedaxis.subaxes]
2563 else:
2564 self.subaxes = [a.createlinkaxis() for a in linkedaxis.subaxes]
2567 class baraxis:
2568 """implementation of a axis for bar graphs
2569 - a bar axes is different from a splitaxis by the way it
2570 selects its subaxes: the convert method gets a list,
2571 where the first entry is a name selecting a subaxis out
2572 of a list; instead of the term "bar" or "subaxis" the term
2573 "item" will be used here
2574 - the baraxis stores a list of names be identify the items;
2575 the names might be of any time (strings, integers, etc.);
2576 the names can be printed as the titles for the items, but
2577 alternatively the names might be transformed by the texts
2578 dictionary, which maps a name to a text to be used to label
2579 the items in the painter
2580 - usually, there is only one subaxis, which is used as
2581 the subaxis for all items
2582 - alternatively it is also possible to use another baraxis
2583 as a multisubaxis; it is copied via the createsubaxis
2584 method whenever another subaxis is needed (by that a
2585 nested bar axis with a different number of subbars at
2586 each item can be created)
2587 - any axis can be a subaxis of a baraxis; if no subaxis
2588 is specified at all, the baraxis simulates a linear
2589 subaxis with a fixed range of 0 to 1
2590 - a splitaxis implements the _Iaxispos for its subaxes
2591 by inheritance from _subaxispos when the multisubaxis
2592 feature is turned on"""
2594 def __init__(self, subaxis=None, multisubaxis=None, title=None,
2595 dist=0.5, firstdist=None, lastdist=None, names=None,
2596 texts={}, painter=baraxispainter()):
2597 """initialize the instance
2598 - subaxis contains a axis to be used as the subaxis
2599 for all items
2600 - multisubaxis might contain another baraxis instance
2601 to be used to construct a new subaxis for each item;
2602 (by that a nested bar axis with a different number
2603 of subbars at each item can be created)
2604 - only one of subaxis or multisubaxis can be set; if neither
2605 of them is set, the baraxis behaves like having a linaxis
2606 as its subaxis with a fixed range 0 to 1
2607 - the title attribute contains the axis title as a string
2608 - the dist is a relsize to be used as the distance between
2609 the items
2610 - the firstdist and lastdist are the distance before the
2611 first and after the last item, respectively; when set
2612 to None (the default), 0.5*dist is used
2613 - names is a predefined list of names to identify the
2614 items; if set, the name list is fixed
2615 - texts is a dictionary transforming a name to a text in
2616 the painter; if a name isn't found in the dictionary
2617 it gets used itself
2618 - the relsize of the baraxis is the sum of the
2619 relsizes including all distances between the items"""
2620 self.dist = dist
2621 if firstdist is not None:
2622 self.firstdist = firstdist
2623 else:
2624 self.firstdist = 0.5 * dist
2625 if lastdist is not None:
2626 self.lastdist = lastdist
2627 else:
2628 self.lastdist = 0.5 * dist
2629 self.relsizes = None
2630 self.fixnames = 0
2631 self.names = []
2632 for name in helper.ensuresequence(names):
2633 self.setname(name)
2634 self.fixnames = names is not None
2635 self.multisubaxis = multisubaxis
2636 if self.multisubaxis is not None:
2637 if subaxis is not None:
2638 raise RuntimeError("either use subaxis or multisubaxis")
2639 self.subaxis = [self.createsubaxis() for name in self.names]
2640 else:
2641 self.subaxis = subaxis
2642 self.title = title
2643 self.fixnames = 0
2644 self.texts = texts
2645 self.painter = painter
2647 def createsubaxis(self):
2648 return baraxis(subaxis=self.multisubaxis.subaxis,
2649 multisubaxis=self.multisubaxis.multisubaxis,
2650 title=self.multisubaxis.title,
2651 dist=self.multisubaxis.dist,
2652 firstdist=self.multisubaxis.firstdist,
2653 lastdist=self.multisubaxis.lastdist,
2654 names=self.multisubaxis.names,
2655 texts=self.multisubaxis.texts,
2656 painter=self.multisubaxis.painter)
2658 def getrange(self):
2659 # TODO: we do not yet have a proper range handling for a baraxis
2660 return None
2662 def setrange(self, min=None, max=None):
2663 # TODO: we do not yet have a proper range handling for a baraxis
2664 raise RuntimeError("range handling for a baraxis is not implemented")
2666 def setname(self, name, *subnames):
2667 """add a name to identify an item at the baraxis
2668 - by using subnames, nested name definitions are
2669 possible
2670 - a style (or the user itself) might use this to
2671 insert new items into a baraxis
2672 - setting self.relsizes to None forces later recalculation"""
2673 if not self.fixnames:
2674 if name not in self.names:
2675 self.relsizes = None
2676 self.names.append(name)
2677 if self.multisubaxis is not None:
2678 self.subaxis.append(self.createsubaxis())
2679 if (not self.fixnames or name in self.names) and len(subnames):
2680 if self.multisubaxis is not None:
2681 if self.subaxis[self.names.index(name)].setname(*subnames):
2682 self.relsizes = None
2683 else:
2684 if self.subaxis.setname(*subnames):
2685 self.relsizes = None
2686 return self.relsizes is not None
2688 def updaterelsizes(self):
2689 # guess what it does: it recalculates relsize attribute
2690 self.relsizes = [i*self.dist + self.firstdist for i in range(len(self.names) + 1)]
2691 self.relsizes[-1] += self.lastdist - self.dist
2692 if self.multisubaxis is not None:
2693 subrelsize = 0
2694 for i in range(1, len(self.relsizes)):
2695 self.subaxis[i-1].updaterelsizes()
2696 subrelsize += self.subaxis[i-1].relsizes[-1]
2697 self.relsizes[i] += subrelsize
2698 else:
2699 if self.subaxis is None:
2700 subrelsize = 1
2701 else:
2702 self.subaxis.updaterelsizes()
2703 subrelsize = self.subaxis.relsizes[-1]
2704 for i in range(1, len(self.relsizes)):
2705 self.relsizes[i] += i * subrelsize
2707 def convert(self, value):
2708 """baraxis convert method
2709 - the value should be a list, where the first entry is
2710 a member of the names (set in the constructor or by the
2711 setname method); this first entry identifies an item in
2712 the baraxis
2713 - following values are passed to the appropriate subaxis
2714 convert method
2715 - when there is no subaxis, the convert method will behave
2716 like having a linaxis from 0 to 1 as subaxis"""
2717 # TODO: proper raising exceptions (which exceptions go thru, which are handled before?)
2718 if not self.relsizes:
2719 self.updaterelsizes()
2720 pos = self.names.index(value[0])
2721 if len(value) == 2:
2722 if self.subaxis is None:
2723 subvalue = value[1]
2724 else:
2725 if self.multisubaxis is not None:
2726 subvalue = value[1] * self.subaxis[pos].relsizes[-1]
2727 else:
2728 subvalue = value[1] * self.subaxis.relsizes[-1]
2729 else:
2730 if self.multisubaxis is not None:
2731 subvalue = self.subaxis[pos].convert(value[1:]) * self.subaxis[pos].relsizes[-1]
2732 else:
2733 subvalue = self.subaxis.convert(value[1:]) * self.subaxis.relsizes[-1]
2734 return (self.relsizes[pos] + subvalue) / float(self.relsizes[-1])
2736 def finish(self, axispos, texrunner):
2737 if self.multisubaxis is not None:
2738 for name, subaxis in zip(self.names, self.subaxis):
2739 subaxis.vmin = self.convert((name, 0))
2740 subaxis.vmax = self.convert((name, 1))
2741 return self.painter.paint(axispos, self)
2743 def createlinkaxis(self, **args):
2744 return linkbaraxis(self, **args)
2747 class linkbaraxis(linkaxis):
2748 """a baraxis linked to an already existing baraxis
2749 - inherits the access to a linked axis -- as before,
2750 basically only the painter is replaced
2751 - it must take care of the creation of linked axes of
2752 the subaxes"""
2754 __implements__ = _Iaxis
2756 def __init__(self, linkedaxis, painter=linkbaraxispainter()):
2757 """initializes the instance
2758 - it gets a axis this linkaxis is linked to
2759 - it gets a painter to be used for this linked axis"""
2760 linkaxis.__init__(self, linkedaxis, painter=painter)
2761 if self.multisubaxis is not None:
2762 self.subaxis = [subaxis.createlinkaxis() for subaxis in self.linkedaxis.subaxis]
2763 elif self.subaxis is not None:
2764 self.subaxis = self.subaxis.createlinkaxis()
2767 ################################################################################
2768 # graph key
2769 ################################################################################
2772 # g = graph.graphxy(key=graph.key())
2773 # g.addkey(graph.key(), ...)
2776 class key:
2778 def __init__(self, dist="0.2 cm", pos = "tr", hinside = 1, vinside = 1, hdist="0.6 cm", vdist="0.4 cm",
2779 symbolwidth="0.5 cm", symbolheight="0.25 cm", symbolspace="0.2 cm",
2780 textattrs=textmodule.vshift.mathaxis):
2781 self.dist_str = dist
2782 self.pos = pos
2783 self.hinside = hinside
2784 self.vinside = vinside
2785 self.hdist_str = hdist
2786 self.vdist_str = vdist
2787 self.symbolwidth_str = symbolwidth
2788 self.symbolheight_str = symbolheight
2789 self.symbolspace_str = symbolspace
2790 self.textattrs = textattrs
2791 self.plotinfos = None
2792 if self.pos in ("tr", "rt"):
2793 self.right = 1
2794 self.top = 1
2795 elif self.pos in ("br", "rb"):
2796 self.right = 1
2797 self.top = 0
2798 elif self.pos in ("tl", "lt"):
2799 self.right = 0
2800 self.top = 1
2801 elif self.pos in ("bl", "lb"):
2802 self.right = 0
2803 self.top = 0
2804 else:
2805 raise RuntimeError("invalid pos attribute")
2807 def setplotinfos(self, *plotinfos):
2808 """set the plotinfos to be used in the key
2809 - call it exactly once
2810 - plotinfo instances with title == None are ignored"""
2811 if self.plotinfos is not None:
2812 raise RuntimeError("setplotinfo is called multiple times")
2813 self.plotinfos = [plotinfo for plotinfo in plotinfos if plotinfo.data.title is not None]
2815 def dolayout(self, graph):
2816 "creates the layout of the key"
2817 self.dist_pt = unit.topt(unit.length(self.dist_str, default_type="v"))
2818 self.hdist_pt = unit.topt(unit.length(self.hdist_str, default_type="v"))
2819 self.vdist_pt = unit.topt(unit.length(self.vdist_str, default_type="v"))
2820 self.symbolwidth_pt = unit.topt(unit.length(self.symbolwidth_str, default_type="v"))
2821 self.symbolheight_pt = unit.topt(unit.length(self.symbolheight_str, default_type="v"))
2822 self.symbolspace_pt = unit.topt(unit.length(self.symbolspace_str, default_type="v"))
2823 self.titles = []
2824 for plotinfo in self.plotinfos:
2825 self.titles.append(graph.texrunner.text_pt(0, 0, plotinfo.data.title, *helper.ensuresequence(self.textattrs)))
2826 box._tile(self.titles, self.dist_pt, 0, -1)
2827 box._linealignequal(self.titles, self.symbolwidth_pt + self.symbolspace_pt, 1, 0)
2829 def bbox(self):
2830 """return a bbox for the key
2831 method should be called after dolayout"""
2832 result = bbox.bbox()
2833 for title in self.titles:
2834 result = result + title.bbox() + bbox._bbox(0, title.center[1] - 0.5 * self.symbolheight_pt,
2835 0, title.center[1] + 0.5 * self.symbolheight_pt)
2836 return result
2838 def paint(self, c, x, y):
2839 """paint the graph key into a canvas c at the position x and y (in postscript points)
2840 - method should be called after dolayout
2841 - the x, y alignment might be calculated by the graph using:
2842 - the bbox of the key as returned by the keys bbox method
2843 - the attributes hdist_pt, vdist_pt, hinside, and vinside of the key
2844 - the dimension and geometry of the graph"""
2845 sc = c.insert(canvas.canvas(trafomodule._translate(x, y)))
2846 for plotinfo, title in zip(self.plotinfos, self.titles):
2847 plotinfo.style.key(sc, 0, -0.5 * self.symbolheight_pt + title.center[1],
2848 self.symbolwidth_pt, self.symbolheight_pt)
2849 sc.insert(title)
2852 ################################################################################
2853 # graph
2854 ################################################################################
2857 class lineaxispos:
2858 """an axispos linear along a line with a fix direction for the ticks"""
2860 __implements__ = _Iaxispos
2862 def __init__(self, convert, x1, y1, x2, y2, fixtickdirection):
2863 """initializes the instance
2864 - only the convert method is needed from the axis
2865 - x1, y1, x2, y2 are PyX length"""
2866 self.convert = convert
2867 self.x1 = x1
2868 self.y1 = y1
2869 self.x2 = x2
2870 self.y2 = y2
2871 self.x1_pt = unit.topt(x1)
2872 self.y1_pt = unit.topt(y1)
2873 self.x2_pt = unit.topt(x2)
2874 self.y2_pt = unit.topt(y2)
2875 self.fixtickdirection = fixtickdirection
2877 def vbasepath(self, v1=None, v2=None):
2878 if v1 is None:
2879 v1 = 0
2880 if v2 is None:
2881 v2 = 1
2882 return path._line((1-v1)*self.x1_pt+v1*self.x2_pt,
2883 (1-v1)*self.y1_pt+v1*self.y2_pt,
2884 (1-v2)*self.x1_pt+v2*self.x2_pt,
2885 (1-v2)*self.y1_pt+v2*self.y2_pt)
2887 def basepath(self, x1=None, x2=None):
2888 if x1 is None:
2889 v1 = 0
2890 else:
2891 v1 = self.convert(x1)
2892 if x2 is None:
2893 v2 = 1
2894 else:
2895 v2 = self.convert(x2)
2896 return path._line((1-v1)*self.x1_pt+v1*self.x2_pt,
2897 (1-v1)*self.y1_pt+v1*self.y2_pt,
2898 (1-v2)*self.x1_pt+v2*self.x2_pt,
2899 (1-v2)*self.y1_pt+v2*self.y2_pt)
2901 def gridpath(self, x):
2902 raise RuntimeError("gridpath not available")
2904 def vgridpath(self, v):
2905 raise RuntimeError("gridpath not available")
2907 def vtickpoint_pt(self, v):
2908 return (1-v)*self.x1_pt+v*self.x2_pt, (1-v)*self.y1_pt+v*self.y2_pt
2910 def vtickpoint(self, v):
2911 return (1-v)*self.x1+v*self.x2, (1-v)*self.y1+v*self.y2
2913 def tickpoint_pt(self, x):
2914 v = self.convert(x)
2915 return (1-v)*self.x1_pt+v*self.x2_pt, (1-v)*self.y1_pt+v*self.y2_pt
2917 def tickpoint(self, x):
2918 v = self.convert(x)
2919 return (1-v)*self.x1+v*self.x2, (1-v)*self.y1+v*self.y2
2921 def tickdirection(self, x):
2922 return self.fixtickdirection
2924 def vtickdirection(self, v):
2925 return self.fixtickdirection
2928 class lineaxisposlinegrid(lineaxispos):
2929 """an axispos linear along a line with a fix direction for the ticks"""
2931 __implements__ = _Iaxispos
2933 def __init__(self, convert, x1, y1, x2, y2, fixtickdirection, startgridlength, endgridlength):
2934 """initializes the instance
2935 - only the convert method is needed from the axis
2936 - x1, y1, x2, y2 are PyX length"""
2937 lineaxispos.__init__(self, convert, x1, y1, x2, y2, fixtickdirection)
2938 self.startgridlength = startgridlength
2939 self.endgridlength = endgridlength
2940 self.startgridlength_pt = unit.topt(self.startgridlength)
2941 self.endgridlength_pt = unit.topt(self.endgridlength)
2943 def gridpath(self, x):
2944 v = self.convert(x)
2945 return path._line((1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.startgridlength_pt,
2946 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.startgridlength_pt,
2947 (1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.endgridlength_pt,
2948 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.endgridlength_pt)
2950 def vgridpath(self, v):
2951 return path._line((1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.startgridlength_pt,
2952 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.startgridlength_pt,
2953 (1-v)*self.x1_pt+v*self.x2_pt+self.fixtickdirection[0]*self.endgridlength_pt,
2954 (1-v)*self.y1_pt+v*self.y2_pt+self.fixtickdirection[1]*self.endgridlength_pt)
2957 class plotinfo:
2959 def __init__(self, data, style):
2960 self.data = data
2961 self.style = style
2965 class graphxy(canvas.canvas):
2967 Names = "x", "y"
2969 class axisposdata:
2971 def __init__(self, type, axispos, tickdirection):
2973 - type == 0: x-axis; type == 1: y-axis
2974 - axispos_pt is the y or x position of the x-axis or y-axis
2975 in postscript points, respectively
2976 - axispos is analogous to axispos, but as a PyX length
2977 - dx and dy is the tick direction
2979 self.type = type
2980 self.axispos = axispos
2981 self.axispos_pt = unit.topt(axispos)
2982 self.tickdirection = tickdirection
2984 def clipcanvas(self):
2985 return self.insert(canvas.canvas(canvas.clip(path._rect(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt))))
2987 def plot(self, data, style=None):
2988 if self.haslayout:
2989 raise RuntimeError("layout setup was already performed")
2990 if style is None:
2991 if helper.issequence(data):
2992 raise RuntimeError("list plot needs an explicit style")
2993 if self.defaultstyle.has_key(data.defaultstyle):
2994 style = self.defaultstyle[data.defaultstyle].iterate()
2995 else:
2996 style = data.defaultstyle()
2997 self.defaultstyle[data.defaultstyle] = style
2998 plotinfos = []
2999 first = 1
3000 for d in helper.ensuresequence(data):
3001 if not first:
3002 style = style.iterate()
3003 first = 0
3004 if d is not None:
3005 d.setstyle(self, style)
3006 plotinfos.append(plotinfo(d, style))
3007 self.plotinfos.extend(plotinfos)
3008 if helper.issequence(data):
3009 return plotinfos
3010 return plotinfos[0]
3012 def addkey(self, key, *plotinfos):
3013 if self.haslayout:
3014 raise RuntimeError("layout setup was already performed")
3015 self.addkeys.append((key, plotinfos))
3017 def pos_pt(self, x, y, xaxis=None, yaxis=None):
3018 if xaxis is None:
3019 xaxis = self.axes["x"]
3020 if yaxis is None:
3021 yaxis = self.axes["y"]
3022 return self.xpos_pt+xaxis.convert(x)*self.width_pt, self.ypos_pt+yaxis.convert(y)*self.height_pt
3024 def pos(self, x, y, xaxis=None, yaxis=None):
3025 if xaxis is None:
3026 xaxis = self.axes["x"]
3027 if yaxis is None:
3028 yaxis = self.axes["y"]
3029 return self.xpos+xaxis.convert(x)*self.width, self.ypos+yaxis.convert(y)*self.height
3031 def vpos_pt(self, vx, vy):
3032 return self.xpos_pt+vx*self.width_pt, self.ypos_pt+vy*self.height_pt
3034 def vpos(self, vx, vy):
3035 return self.xpos+vx*self.width, self.ypos+vy*self.height
3037 # def xbaseline(self, x1=None, x2=None, axis=None):
3038 # if axis is None:
3039 # axis = self.axes["x"]
3040 # if x1 is not None:
3041 # v1 = axis.convert(x1)
3042 # else:
3043 # v1 = 0
3044 # if x2 is not None:
3045 # v2 = axis.convert(x2)
3046 # else:
3047 # v2 = 1
3048 # return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3049 # self._xpos+v2*self._width, axis.axisposdata._axispos)
3051 # def ybaseline(self, x1=None, x2=None, axis=None):
3052 # if axis is None:
3053 # axis = self.axes["y"]
3054 # if x1 is not None:
3055 # v1 = axis.convert(x1)
3056 # else:
3057 # v1 = 0
3058 # if x2 is not None:
3059 # v2 = axis.convert(x2)
3060 # else:
3061 # v2 = 1
3062 # return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3063 # axis.axisposdata._axispos, self._ypos+v2*self._height)
3065 # def vxbaseline(self, v1=None, v2=None, axis=None):
3066 # if axis is None:
3067 # axis = self.axes["x"]
3068 # if v1 is None:
3069 # v1 = 0
3070 # if v2 is None:
3071 # v2 = 1
3072 # return path._line(self._xpos+v1*self._width, axis.axisposdata._axispos,
3073 # self._xpos+v2*self._width, axis.axisposdata._axispos)
3075 # def vybaseline(self, v1=None, v2=None, axis=None):
3076 # if axis is None:
3077 # axis = self.axes["y"]
3078 # if v1 is None:
3079 # v1 = 0
3080 # if v2 is None:
3081 # v2 = 1
3082 # return path._line(axis.axisposdata._axispos, self._ypos+v1*self._height,
3083 # axis.axisposdata._axispos, self._ypos+v2*self._height)
3085 # def xgridline(self, x, axis=None):
3086 # if axis is None:
3087 # axis = self.axes["x"]
3088 # v = axis.convert(x)
3089 # return path._line(self._xpos+v*self._width, self._ypos,
3090 # self._xpos+v*self._width, self._ypos+self._height)
3092 # def ygridline(self, x, axis=None):
3093 # if axis is None:
3094 # axis = self.axes["y"]
3095 # v = axis.convert(x)
3096 # return path._line(self._xpos, self._ypos+v*self._height,
3097 # self._xpos+self._width, self._ypos+v*self._height)
3099 # def vxgridline(self, v, axis=None):
3100 # if axis is None:
3101 # axis = self.axes["x"]
3102 # return path._line(self._xpos+v*self._width, self._ypos,
3103 # self._xpos+v*self._width, self._ypos+self._height)
3105 # def vygridline(self, v, axis=None):
3106 # if axis is None:
3107 # axis = self.axes["y"]
3108 # return path._line(self._xpos, self._ypos+v*self._height,
3109 # self._xpos+self._width, self._ypos+v*self._height)
3111 # def _xtickpoint(self, x, axis=None):
3112 # if axis is None:
3113 # axis = self.axes["x"]
3114 # return self._xpos+axis.convert(x)*self._width, axis.axisposdata._axispos
3116 # def _ytickpoint(self, x, axis=None):
3117 # if axis is None:
3118 # axis = self.axes["y"]
3119 # return axis.axisposdata._axispos, self._ypos+axis.convert(x)*self._height
3121 # def xtickpoint(self, x, axis=None):
3122 # if axis is None:
3123 # axis = self.axes["x"]
3124 # return self.xpos+axis.convert(x)*self.width, axis.axisposdata.axispos
3126 # def ytickpoint(self, x, axis=None):
3127 # if axis is None:
3128 # axis = self.axes["y"]
3129 # return axis.axisposdata.axispos, self.ypos+axis.convert(x)*self.height
3131 # def _vxtickpoint(self, v, axis=None):
3132 # if axis is None:
3133 # axis = self.axes["x"]
3134 # return self._xpos+v*self._width, axis.axisposdata._axispos
3136 # def _vytickpoint(self, v, axis=None):
3137 # if axis is None:
3138 # axis = self.axes["y"]
3139 # return axis.axisposdata._axispos, self._ypos+v*self._height
3141 # def vxtickpoint(self, v, axis=None):
3142 # if axis is None:
3143 # axis = self.axes["x"]
3144 # return self.xpos+v*self.width, axis.axisposdata.axispos
3146 # def vytickpoint(self, v, axis=None):
3147 # if axis is None:
3148 # axis = self.axes["y"]
3149 # return axis.axisposdata.axispos, self.ypos+v*self.height
3151 # def xtickdirection(self, x, axis=None):
3152 # if axis is None:
3153 # axis = self.axes["x"]
3154 # return axis.axisposdata.tickdirection
3156 # def ytickdirection(self, x, axis=None):
3157 # if axis is None:
3158 # axis = self.axes["y"]
3159 # return axis.axisposdata.tickdirection
3161 # def vxtickdirection(self, v, axis=None):
3162 # if axis is None:
3163 # axis = self.axes["x"]
3164 # return axis.axisposdata.tickdirection
3166 # def vytickdirection(self, v, axis=None):
3167 # if axis is None:
3168 # axis = self.axes["y"]
3169 # return axis.axisposdata.tickdirection
3171 def _addpos(self, x, y, dx, dy):
3172 return x+dx, y+dy
3174 def _connect(self, x1, y1, x2, y2):
3175 return path._lineto(x2, y2)
3177 def keynum(self, key):
3178 try:
3179 while key[0] in string.letters:
3180 key = key[1:]
3181 return int(key)
3182 except IndexError:
3183 return 1
3185 def gatherranges(self):
3186 ranges = {}
3187 for plotinfo in self.plotinfos:
3188 pdranges = plotinfo.data.getranges()
3189 if pdranges is not None:
3190 for key in pdranges.keys():
3191 if key not in ranges.keys():
3192 ranges[key] = pdranges[key]
3193 else:
3194 ranges[key] = (min(ranges[key][0], pdranges[key][0]),
3195 max(ranges[key][1], pdranges[key][1]))
3196 # known ranges are also set as ranges for the axes
3197 for key, axis in self.axes.items():
3198 if key in ranges.keys():
3199 axis.setrange(*ranges[key])
3200 ranges[key] = axis.getrange()
3201 if ranges[key] is None:
3202 del ranges[key]
3203 return ranges
3205 def removedomethod(self, method):
3206 hadmethod = 0
3207 while 1:
3208 try:
3209 self.domethods.remove(method)
3210 hadmethod = 1
3211 except ValueError:
3212 return hadmethod
3214 def dolayout(self):
3215 if not self.removedomethod(self.dolayout): return
3216 self.haslayout = 1
3217 # create list of ranges
3218 # 1. gather ranges
3219 ranges = self.gatherranges()
3220 # 2. calculate additional ranges out of known ranges
3221 for plotinfo in self.plotinfos:
3222 plotinfo.data.setranges(ranges)
3223 # 3. gather ranges again
3224 self.gatherranges()
3225 # do the layout for all axes
3226 axesdist = unit.length(self.axesdist_str, default_type="v")
3227 XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3228 YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3229 xaxisextents = [0, 0]
3230 yaxisextents = [0, 0]
3231 needxaxisdist = [0, 0]
3232 needyaxisdist = [0, 0]
3233 items = list(self.axes.items())
3234 items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3235 for key, axis in items:
3236 num = self.keynum(key)
3237 num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3238 num3 = 2 * (num % 2) - 1 # x1 -> 1, x2 -> -1, x3 -> 1, x4 -> -1, ...
3239 if XPattern.match(key):
3240 if needxaxisdist[num2]:
3241 xaxisextents[num2] += axesdist
3242 self.axespos[key] = lineaxisposlinegrid(self.axes[key].convert,
3243 self.xpos,
3244 self.ypos + num2*self.height - num3*xaxisextents[num2],
3245 self.xpos + self.width,
3246 self.ypos + num2*self.height - num3*xaxisextents[num2],
3247 (0, num3),
3248 xaxisextents[num2], xaxisextents[num2] + self.height)
3249 if num == 1:
3250 self.xbasepath = self.axespos[key].basepath
3251 self.xvbasepath = self.axespos[key].vbasepath
3252 self.xgridpath = self.axespos[key].gridpath
3253 self.xvgridpath = self.axespos[key].vgridpath
3254 self.xtickpoint_pt = self.axespos[key].tickpoint_pt
3255 self.xtickpoint = self.axespos[key].tickpoint
3256 self.xvtickpoint_pt = self.axespos[key].vtickpoint_pt
3257 self.xvtickpoint = self.axespos[key].tickpoint
3258 self.xtickdirection = self.axespos[key].tickdirection
3259 self.xvtickdirection = self.axespos[key].vtickdirection
3260 elif YPattern.match(key):
3261 if needyaxisdist[num2]:
3262 yaxisextents[num2] += axesdist
3263 self.axespos[key] = lineaxisposlinegrid(self.axes[key].convert,
3264 self.xpos + num2*self.width - num3*yaxisextents[num2],
3265 self.ypos,
3266 self.xpos + num2*self.width - num3*yaxisextents[num2],
3267 self.ypos + self.height,
3268 (num3, 0),
3269 yaxisextents[num2], yaxisextents[num2] + self.width)
3270 if num == 1:
3271 self.ybasepath = self.axespos[key].basepath
3272 self.yvbasepath = self.axespos[key].vbasepath
3273 self.ygridpath = self.axespos[key].gridpath
3274 self.yvgridpath = self.axespos[key].vgridpath
3275 self.ytickpoint_pt = self.axespos[key].tickpoint_pt
3276 self.ytickpoint = self.axespos[key].tickpoint
3277 self.yvtickpoint_pt = self.axespos[key].vtickpoint_pt
3278 self.yvtickpoint = self.axespos[key].tickpoint
3279 self.ytickdirection = self.axespos[key].tickdirection
3280 self.yvtickdirection = self.axespos[key].vtickdirection
3281 else:
3282 raise ValueError("Axis key '%s' not allowed" % key)
3283 self.axescanvas[key] = axis.finish(self.axespos[key], self.texrunner)
3284 if XPattern.match(key):
3285 xaxisextents[num2] += self.axescanvas[key].extent
3286 needxaxisdist[num2] = 1
3287 if YPattern.match(key):
3288 yaxisextents[num2] += self.axescanvas[key].extent
3289 needyaxisdist[num2] = 1
3291 def dobackground(self):
3292 self.dolayout()
3293 if not self.removedomethod(self.dobackground): return
3294 if self.backgroundattrs is not None:
3295 self.draw(path._rect(self.xpos_pt, self.ypos_pt, self.width_pt, self.height_pt),
3296 *helper.ensuresequence(self.backgroundattrs))
3298 def doaxes(self):
3299 self.dolayout()
3300 if not self.removedomethod(self.doaxes): return
3301 for axiscanvas in self.axescanvas.values():
3302 self.insert(axiscanvas)
3304 def dodata(self):
3305 self.dolayout()
3306 if not self.removedomethod(self.dodata): return
3307 for plotinfo in self.plotinfos:
3308 plotinfo.data.draw(self)
3310 def _dokey(self, key, *plotinfos):
3311 key.setplotinfos(*plotinfos)
3312 key.dolayout(self)
3313 bbox = key.bbox()
3314 if key.right:
3315 if key.hinside:
3316 x = self.xpos_pt + self.width_pt - bbox.urx - key.hdist_pt
3317 else:
3318 x = self.xpos_pt + self.width_pt - bbox.llx + key.hdist_pt
3319 else:
3320 if key.hinside:
3321 x = self.xpos_pt - bbox.llx + key.hdist_pt
3322 else:
3323 x = self.xpos_pt - bbox.urx - key.hdist_pt
3324 if key.top:
3325 if key.vinside:
3326 y = self.ypos_pt + self.height_pt - bbox.ury - key.vdist_pt
3327 else:
3328 y = self.ypos_pt + self.height_pt - bbox.lly + key.vdist_pt
3329 else:
3330 if key.vinside:
3331 y = self.ypos_pt - bbox.lly + key.vdist_pt
3332 else:
3333 y = self.ypos_pt - bbox.ury - key.vdist_pt
3334 key.paint(self, x, y)
3336 def dokey(self):
3337 self.dolayout()
3338 if not self.removedomethod(self.dokey): return
3339 if self.key is not None:
3340 self._dokey(self.key, *self.plotinfos)
3341 for key, plotinfos in self.addkeys:
3342 self._dokey(key, *plotinfos)
3344 def finish(self):
3345 while len(self.domethods):
3346 self.domethods[0]()
3348 def initwidthheight(self, width, height, ratio):
3349 if (width is not None) and (height is None):
3350 self.width = unit.length(width)
3351 self.height = (1.0/ratio) * self.width
3352 elif (height is not None) and (width is None):
3353 self.height = unit.length(height)
3354 self.width = ratio * self.height
3355 else:
3356 self.width = unit.length(width)
3357 self.height = unit.length(height)
3358 self.width_pt = unit.topt(self.width)
3359 self.height_pt = unit.topt(self.height)
3360 if self.width_pt <= 0: raise ValueError("width <= 0")
3361 if self.height_pt <= 0: raise ValueError("height <= 0")
3363 def initaxes(self, axes, addlinkaxes=0):
3364 for key in self.Names:
3365 if not axes.has_key(key):
3366 axes[key] = linaxis()
3367 elif axes[key] is None:
3368 del axes[key]
3369 if addlinkaxes:
3370 if not axes.has_key(key + "2") and axes.has_key(key):
3371 axes[key + "2"] = axes[key].createlinkaxis()
3372 elif axes[key + "2"] is None:
3373 del axes[key + "2"]
3374 self.axes = axes
3376 def __init__(self, xpos=0, ypos=0, width=None, height=None, ratio=goldenmean,
3377 key=None, backgroundattrs=None, axesdist="0.8 cm", **axes):
3378 canvas.canvas.__init__(self)
3379 self.xpos = unit.length(xpos)
3380 self.ypos = unit.length(ypos)
3381 self.xpos_pt = unit.topt(self.xpos)
3382 self.ypos_pt = unit.topt(self.ypos)
3383 self.initwidthheight(width, height, ratio)
3384 self.initaxes(axes, 1)
3385 self.axescanvas = {}
3386 self.axespos = {}
3387 self.key = key
3388 self.backgroundattrs = backgroundattrs
3389 self.axesdist_str = axesdist
3390 self.plotinfos = []
3391 self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata, self.dokey]
3392 self.haslayout = 0
3393 self.defaultstyle = {}
3394 self.addkeys = []
3396 def bbox(self):
3397 self.finish()
3398 return canvas.canvas.bbox(self)
3400 def write(self, file):
3401 self.finish()
3402 canvas.canvas.write(self, file)
3406 # some thoughts, but deferred right now
3408 # class graphxyz(graphxy):
3410 # Names = "x", "y", "z"
3412 # def _vxtickpoint(self, axis, v):
3413 # return self._vpos(v, axis.vypos, axis.vzpos)
3415 # def _vytickpoint(self, axis, v):
3416 # return self._vpos(axis.vxpos, v, axis.vzpos)
3418 # def _vztickpoint(self, axis, v):
3419 # return self._vpos(axis.vxpos, axis.vypos, v)
3421 # def vxtickdirection(self, axis, v):
3422 # x1, y1 = self._vpos(v, axis.vypos, axis.vzpos)
3423 # x2, y2 = self._vpos(v, 0.5, 0)
3424 # dx, dy = x1 - x2, y1 - y2
3425 # norm = math.sqrt(dx*dx + dy*dy)
3426 # return dx/norm, dy/norm
3428 # def vytickdirection(self, axis, v):
3429 # x1, y1 = self._vpos(axis.vxpos, v, axis.vzpos)
3430 # x2, y2 = self._vpos(0.5, v, 0)
3431 # dx, dy = x1 - x2, y1 - y2
3432 # norm = math.sqrt(dx*dx + dy*dy)
3433 # return dx/norm, dy/norm
3435 # def vztickdirection(self, axis, v):
3436 # return -1, 0
3437 # x1, y1 = self._vpos(axis.vxpos, axis.vypos, v)
3438 # x2, y2 = self._vpos(0.5, 0.5, v)
3439 # dx, dy = x1 - x2, y1 - y2
3440 # norm = math.sqrt(dx*dx + dy*dy)
3441 # return dx/norm, dy/norm
3443 # def _pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3444 # if xaxis is None: xaxis = self.axes["x"]
3445 # if yaxis is None: yaxis = self.axes["y"]
3446 # if zaxis is None: zaxis = self.axes["z"]
3447 # return self._vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3449 # def pos(self, x, y, z, xaxis=None, yaxis=None, zaxis=None):
3450 # if xaxis is None: xaxis = self.axes["x"]
3451 # if yaxis is None: yaxis = self.axes["y"]
3452 # if zaxis is None: zaxis = self.axes["z"]
3453 # return self.vpos(xaxis.convert(x), yaxis.convert(y), zaxis.convert(z))
3455 # def _vpos(self, vx, vy, vz):
3456 # x, y, z = (vx - 0.5)*self._depth, (vy - 0.5)*self._width, (vz - 0.5)*self._height
3457 # d0 = float(self.a[0]*self.b[1]*(z-self.eye[2])
3458 # + self.a[2]*self.b[0]*(y-self.eye[1])
3459 # + self.a[1]*self.b[2]*(x-self.eye[0])
3460 # - self.a[2]*self.b[1]*(x-self.eye[0])
3461 # - self.a[0]*self.b[2]*(y-self.eye[1])
3462 # - self.a[1]*self.b[0]*(z-self.eye[2]))
3463 # da = (self.eye[0]*self.b[1]*(z-self.eye[2])
3464 # + self.eye[2]*self.b[0]*(y-self.eye[1])
3465 # + self.eye[1]*self.b[2]*(x-self.eye[0])
3466 # - self.eye[2]*self.b[1]*(x-self.eye[0])
3467 # - self.eye[0]*self.b[2]*(y-self.eye[1])
3468 # - self.eye[1]*self.b[0]*(z-self.eye[2]))
3469 # db = (self.a[0]*self.eye[1]*(z-self.eye[2])
3470 # + self.a[2]*self.eye[0]*(y-self.eye[1])
3471 # + self.a[1]*self.eye[2]*(x-self.eye[0])
3472 # - self.a[2]*self.eye[1]*(x-self.eye[0])
3473 # - self.a[0]*self.eye[2]*(y-self.eye[1])
3474 # - self.a[1]*self.eye[0]*(z-self.eye[2]))
3475 # return da/d0 + self._xpos, db/d0 + self._ypos
3477 # def vpos(self, vx, vy, vz):
3478 # tx, ty = self._vpos(vx, vy, vz)
3479 # return unit.t_pt(tx), unit.t_pt(ty)
3481 # def xbaseline(self, axis, x1, x2, xaxis=None):
3482 # if xaxis is None: xaxis = self.axes["x"]
3483 # return self.vxbaseline(axis, xaxis.convert(x1), xaxis.convert(x2))
3485 # def ybaseline(self, axis, y1, y2, yaxis=None):
3486 # if yaxis is None: yaxis = self.axes["y"]
3487 # return self.vybaseline(axis, yaxis.convert(y1), yaxis.convert(y2))
3489 # def zbaseline(self, axis, z1, z2, zaxis=None):
3490 # if zaxis is None: zaxis = self.axes["z"]
3491 # return self.vzbaseline(axis, zaxis.convert(z1), zaxis.convert(z2))
3493 # def vxbaseline(self, axis, v1, v2):
3494 # return (path._line(*(self._vpos(v1, 0, 0) + self._vpos(v2, 0, 0))) +
3495 # path._line(*(self._vpos(v1, 0, 1) + self._vpos(v2, 0, 1))) +
3496 # path._line(*(self._vpos(v1, 1, 1) + self._vpos(v2, 1, 1))) +
3497 # path._line(*(self._vpos(v1, 1, 0) + self._vpos(v2, 1, 0))))
3499 # def vybaseline(self, axis, v1, v2):
3500 # return (path._line(*(self._vpos(0, v1, 0) + self._vpos(0, v2, 0))) +
3501 # path._line(*(self._vpos(0, v1, 1) + self._vpos(0, v2, 1))) +
3502 # path._line(*(self._vpos(1, v1, 1) + self._vpos(1, v2, 1))) +
3503 # path._line(*(self._vpos(1, v1, 0) + self._vpos(1, v2, 0))))
3505 # def vzbaseline(self, axis, v1, v2):
3506 # return (path._line(*(self._vpos(0, 0, v1) + self._vpos(0, 0, v2))) +
3507 # path._line(*(self._vpos(0, 1, v1) + self._vpos(0, 1, v2))) +
3508 # path._line(*(self._vpos(1, 1, v1) + self._vpos(1, 1, v2))) +
3509 # path._line(*(self._vpos(1, 0, v1) + self._vpos(1, 0, v2))))
3511 # def xgridpath(self, x, xaxis=None):
3512 # assert 0
3513 # if xaxis is None: xaxis = self.axes["x"]
3514 # v = xaxis.convert(x)
3515 # return path._line(self._xpos+v*self._width, self._ypos,
3516 # self._xpos+v*self._width, self._ypos+self._height)
3518 # def ygridpath(self, y, yaxis=None):
3519 # assert 0
3520 # if yaxis is None: yaxis = self.axes["y"]
3521 # v = yaxis.convert(y)
3522 # return path._line(self._xpos, self._ypos+v*self._height,
3523 # self._xpos+self._width, self._ypos+v*self._height)
3525 # def zgridpath(self, z, zaxis=None):
3526 # assert 0
3527 # if zaxis is None: zaxis = self.axes["z"]
3528 # v = zaxis.convert(z)
3529 # return path._line(self._xpos, self._zpos+v*self._height,
3530 # self._xpos+self._width, self._zpos+v*self._height)
3532 # def vxgridpath(self, v):
3533 # return path.path(path._moveto(*self._vpos(v, 0, 0)),
3534 # path._lineto(*self._vpos(v, 0, 1)),
3535 # path._lineto(*self._vpos(v, 1, 1)),
3536 # path._lineto(*self._vpos(v, 1, 0)),
3537 # path.closepath())
3539 # def vygridpath(self, v):
3540 # return path.path(path._moveto(*self._vpos(0, v, 0)),
3541 # path._lineto(*self._vpos(0, v, 1)),
3542 # path._lineto(*self._vpos(1, v, 1)),
3543 # path._lineto(*self._vpos(1, v, 0)),
3544 # path.closepath())
3546 # def vzgridpath(self, v):
3547 # return path.path(path._moveto(*self._vpos(0, 0, v)),
3548 # path._lineto(*self._vpos(0, 1, v)),
3549 # path._lineto(*self._vpos(1, 1, v)),
3550 # path._lineto(*self._vpos(1, 0, v)),
3551 # path.closepath())
3553 # def _addpos(self, x, y, dx, dy):
3554 # assert 0
3555 # return x+dx, y+dy
3557 # def _connect(self, x1, y1, x2, y2):
3558 # assert 0
3559 # return path._lineto(x2, y2)
3561 # def doaxes(self):
3562 # self.dolayout()
3563 # if not self.removedomethod(self.doaxes): return
3564 # axesdist = unit.topt(unit.length(self.axesdist_str, default_type="v"))
3565 # XPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[0])
3566 # YPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[1])
3567 # ZPattern = re.compile(r"%s([2-9]|[1-9][0-9]+)?$" % self.Names[2])
3568 # items = list(self.axes.items())
3569 # items.sort() #TODO: alphabetical sorting breaks for axis numbers bigger than 9
3570 # for key, axis in items:
3571 # num = self.keynum(key)
3572 # num2 = 1 - num % 2 # x1 -> 0, x2 -> 1, x3 -> 0, x4 -> 1, ...
3573 # num3 = 1 - 2 * (num % 2) # x1 -> -1, x2 -> 1, x3 -> -1, x4 -> 1, ...
3574 # if XPattern.match(key):
3575 # axis.vypos = 0
3576 # axis.vzpos = 0
3577 # axis._vtickpoint = self._vxtickpoint
3578 # axis.vgridpath = self.vxgridpath
3579 # axis.vbaseline = self.vxbaseline
3580 # axis.vtickdirection = self.vxtickdirection
3581 # elif YPattern.match(key):
3582 # axis.vxpos = 0
3583 # axis.vzpos = 0
3584 # axis._vtickpoint = self._vytickpoint
3585 # axis.vgridpath = self.vygridpath
3586 # axis.vbaseline = self.vybaseline
3587 # axis.vtickdirection = self.vytickdirection
3588 # elif ZPattern.match(key):
3589 # axis.vxpos = 0
3590 # axis.vypos = 0
3591 # axis._vtickpoint = self._vztickpoint
3592 # axis.vgridpath = self.vzgridpath
3593 # axis.vbaseline = self.vzbaseline
3594 # axis.vtickdirection = self.vztickdirection
3595 # else:
3596 # raise ValueError("Axis key '%s' not allowed" % key)
3597 # if axis.painter is not None:
3598 # axis.dopaint(self)
3599 # # if XPattern.match(key):
3600 # # self._xaxisextents[num2] += axis._extent
3601 # # needxaxisdist[num2] = 1
3602 # # if YPattern.match(key):
3603 # # self._yaxisextents[num2] += axis._extent
3604 # # needyaxisdist[num2] = 1
3606 # def __init__(self, tex, xpos=0, ypos=0, width=None, height=None, depth=None,
3607 # phi=30, theta=30, distance=1,
3608 # backgroundattrs=None, axesdist="0.8 cm", **axes):
3609 # canvas.canvas.__init__(self)
3610 # self.tex = tex
3611 # self.xpos = xpos
3612 # self.ypos = ypos
3613 # self._xpos = unit.topt(xpos)
3614 # self._ypos = unit.topt(ypos)
3615 # self._width = unit.topt(width)
3616 # self._height = unit.topt(height)
3617 # self._depth = unit.topt(depth)
3618 # self.width = width
3619 # self.height = height
3620 # self.depth = depth
3621 # if self._width <= 0: raise ValueError("width < 0")
3622 # if self._height <= 0: raise ValueError("height < 0")
3623 # if self._depth <= 0: raise ValueError("height < 0")
3624 # self._distance = distance*math.sqrt(self._width*self._width+
3625 # self._height*self._height+
3626 # self._depth*self._depth)
3627 # phi *= -math.pi/180
3628 # theta *= math.pi/180
3629 # self.a = (-math.sin(phi), math.cos(phi), 0)
3630 # self.b = (-math.cos(phi)*math.sin(theta),
3631 # -math.sin(phi)*math.sin(theta),
3632 # math.cos(theta))
3633 # self.eye = (self._distance*math.cos(phi)*math.cos(theta),
3634 # self._distance*math.sin(phi)*math.cos(theta),
3635 # self._distance*math.sin(theta))
3636 # self.initaxes(axes)
3637 # self.axesdist_str = axesdist
3638 # self.backgroundattrs = backgroundattrs
3640 # self.data = []
3641 # self.domethods = [self.dolayout, self.dobackground, self.doaxes, self.dodata]
3642 # self.haslayout = 0
3643 # self.defaultstyle = {}
3645 # def bbox(self):
3646 # self.finish()
3647 # return bbox._bbox(self._xpos - 200, self._ypos - 200, self._xpos + 200, self._ypos + 200)
3650 ################################################################################
3651 # attr changers
3652 ################################################################################
3655 #class _Ichangeattr:
3656 # """attribute changer
3657 # is an iterator for attributes where an attribute
3658 # is not refered by just a number (like for a sequence),
3659 # but also by the number of attributes requested
3660 # by calls of the next method (like for an color palette)
3661 # (you should ensure to call all needed next before the attr)
3663 # the attribute itself is implemented by overloading the _attr method"""
3665 # def attr(self):
3666 # "get an attribute"
3668 # def next(self):
3669 # "get an attribute changer for the next attribute"
3672 class _changeattr: pass
3675 class changeattr(_changeattr):
3677 def __init__(self):
3678 self.counter = 1
3680 def getattr(self):
3681 return self.attr(0)
3683 def iterate(self):
3684 newindex = self.counter
3685 self.counter += 1
3686 return refattr(self, newindex)
3689 class refattr(_changeattr):
3691 def __init__(self, ref, index):
3692 self.ref = ref
3693 self.index = index
3695 def getattr(self):
3696 return self.ref.attr(self.index)
3698 def iterate(self):
3699 return self.ref.iterate()
3702 # helper routines for a using attrs
3704 def _getattr(attr):
3705 "get attr out of a attr/changeattr"
3706 if isinstance(attr, _changeattr):
3707 return attr.getattr()
3708 return attr
3711 def _getattrs(attrs):
3712 "get attrs out of a list of attr/changeattr"
3713 if attrs is not None:
3714 result = []
3715 for attr in helper.ensuresequence(attrs):
3716 if isinstance(attr, _changeattr):
3717 attr = attr.getattr()
3718 if attr is not None:
3719 result.append(attr)
3720 if len(result) or not len(attrs):
3721 return result
3724 def _iterateattr(attr):
3725 "perform next to a attr/changeattr"
3726 if isinstance(attr, _changeattr):
3727 return attr.iterate()
3728 return attr
3731 def _iterateattrs(attrs):
3732 "perform next to a list of attr/changeattr"
3733 if attrs is not None:
3734 result = []
3735 for attr in helper.ensuresequence(attrs):
3736 if isinstance(attr, _changeattr):
3737 result.append(attr.iterate())
3738 else:
3739 result.append(attr)
3740 return result
3743 class changecolor(changeattr):
3745 def __init__(self, palette):
3746 changeattr.__init__(self)
3747 self.palette = palette
3749 def attr(self, index):
3750 if self.counter != 1:
3751 return self.palette.getcolor(index/float(self.counter-1))
3752 else:
3753 return self.palette.getcolor(0)
3756 class _changecolorgray(changecolor):
3758 def __init__(self, palette=color.palette.Gray):
3759 changecolor.__init__(self, palette)
3761 _changecolorgrey = _changecolorgray
3764 class _changecolorreversegray(changecolor):
3766 def __init__(self, palette=color.palette.ReverseGray):
3767 changecolor.__init__(self, palette)
3769 _changecolorreversegrey = _changecolorreversegray
3772 class _changecolorredblack(changecolor):
3774 def __init__(self, palette=color.palette.RedBlack):
3775 changecolor.__init__(self, palette)
3778 class _changecolorblackred(changecolor):
3780 def __init__(self, palette=color.palette.BlackRed):
3781 changecolor.__init__(self, palette)
3784 class _changecolorredwhite(changecolor):
3786 def __init__(self, palette=color.palette.RedWhite):
3787 changecolor.__init__(self, palette)
3790 class _changecolorwhitered(changecolor):
3792 def __init__(self, palette=color.palette.WhiteRed):
3793 changecolor.__init__(self, palette)
3796 class _changecolorgreenblack(changecolor):
3798 def __init__(self, palette=color.palette.GreenBlack):
3799 changecolor.__init__(self, palette)
3802 class _changecolorblackgreen(changecolor):
3804 def __init__(self, palette=color.palette.BlackGreen):
3805 changecolor.__init__(self, palette)
3808 class _changecolorgreenwhite(changecolor):
3810 def __init__(self, palette=color.palette.GreenWhite):
3811 changecolor.__init__(self, palette)
3814 class _changecolorwhitegreen(changecolor):
3816 def __init__(self, palette=color.palette.WhiteGreen):
3817 changecolor.__init__(self, palette)
3820 class _changecolorblueblack(changecolor):
3822 def __init__(self, palette=color.palette.BlueBlack):
3823 changecolor.__init__(self, palette)
3826 class _changecolorblackblue(changecolor):
3828 def __init__(self, palette=color.palette.BlackBlue):
3829 changecolor.__init__(self, palette)
3832 class _changecolorbluewhite(changecolor):
3834 def __init__(self, palette=color.palette.BlueWhite):
3835 changecolor.__init__(self, palette)
3838 class _changecolorwhiteblue(changecolor):
3840 def __init__(self, palette=color.palette.WhiteBlue):
3841 changecolor.__init__(self, palette)
3844 class _changecolorredgreen(changecolor):
3846 def __init__(self, palette=color.palette.RedGreen):
3847 changecolor.__init__(self, palette)
3850 class _changecolorredblue(changecolor):
3852 def __init__(self, palette=color.palette.RedBlue):
3853 changecolor.__init__(self, palette)
3856 class _changecolorgreenred(changecolor):
3858 def __init__(self, palette=color.palette.GreenRed):
3859 changecolor.__init__(self, palette)
3862 class _changecolorgreenblue(changecolor):
3864 def __init__(self, palette=color.palette.GreenBlue):
3865 changecolor.__init__(self, palette)
3868 class _changecolorbluered(changecolor):
3870 def __init__(self, palette=color.palette.BlueRed):
3871 changecolor.__init__(self, palette)
3874 class _changecolorbluegreen(changecolor):
3876 def __init__(self, palette=color.palette.BlueGreen):
3877 changecolor.__init__(self, palette)
3880 class _changecolorrainbow(changecolor):
3882 def __init__(self, palette=color.palette.Rainbow):
3883 changecolor.__init__(self, palette)
3886 class _changecolorreverserainbow(changecolor):
3888 def __init__(self, palette=color.palette.ReverseRainbow):
3889 changecolor.__init__(self, palette)
3892 class _changecolorhue(changecolor):
3894 def __init__(self, palette=color.palette.Hue):
3895 changecolor.__init__(self, palette)
3898 class _changecolorreversehue(changecolor):
3900 def __init__(self, palette=color.palette.ReverseHue):
3901 changecolor.__init__(self, palette)
3904 changecolor.Gray = _changecolorgray
3905 changecolor.Grey = _changecolorgrey
3906 changecolor.Reversegray = _changecolorreversegray
3907 changecolor.Reversegrey = _changecolorreversegrey
3908 changecolor.RedBlack = _changecolorredblack
3909 changecolor.BlackRed = _changecolorblackred
3910 changecolor.RedWhite = _changecolorredwhite
3911 changecolor.WhiteRed = _changecolorwhitered
3912 changecolor.GreenBlack = _changecolorgreenblack
3913 changecolor.BlackGreen = _changecolorblackgreen
3914 changecolor.GreenWhite = _changecolorgreenwhite
3915 changecolor.WhiteGreen = _changecolorwhitegreen
3916 changecolor.BlueBlack = _changecolorblueblack
3917 changecolor.BlackBlue = _changecolorblackblue
3918 changecolor.BlueWhite = _changecolorbluewhite
3919 changecolor.WhiteBlue = _changecolorwhiteblue
3920 changecolor.RedGreen = _changecolorredgreen
3921 changecolor.RedBlue = _changecolorredblue
3922 changecolor.GreenRed = _changecolorgreenred
3923 changecolor.GreenBlue = _changecolorgreenblue
3924 changecolor.BlueRed = _changecolorbluered
3925 changecolor.BlueGreen = _changecolorbluegreen
3926 changecolor.Rainbow = _changecolorrainbow
3927 changecolor.ReverseRainbow = _changecolorreverserainbow
3928 changecolor.Hue = _changecolorhue
3929 changecolor.ReverseHue = _changecolorreversehue
3932 class changesequence(changeattr):
3933 "cycles through a list"
3935 def __init__(self, *sequence):
3936 changeattr.__init__(self)
3937 if not len(sequence):
3938 sequence = self.defaultsequence
3939 self.sequence = sequence
3941 def attr(self, index):
3942 return self.sequence[index % len(self.sequence)]
3945 class changelinestyle(changesequence):
3946 defaultsequence = (style.linestyle.solid,
3947 style.linestyle.dashed,
3948 style.linestyle.dotted,
3949 style.linestyle.dashdotted)
3952 class changestrokedfilled(changesequence):
3953 defaultsequence = (deco.stroked(), deco.filled())
3956 class changefilledstroked(changesequence):
3957 defaultsequence = (deco.filled(), deco.stroked())
3961 ################################################################################
3962 # styles
3963 ################################################################################
3966 class symbol:
3968 def cross(self, x, y):
3969 return (path._moveto(x-0.5*self.size_pt, y-0.5*self.size_pt),
3970 path._lineto(x+0.5*self.size_pt, y+0.5*self.size_pt),
3971 path._moveto(x-0.5*self.size_pt, y+0.5*self.size_pt),
3972 path._lineto(x+0.5*self.size_pt, y-0.5*self.size_pt))
3974 def plus(self, x, y):
3975 return (path._moveto(x-0.707106781*self.size_pt, y),
3976 path._lineto(x+0.707106781*self.size_pt, y),
3977 path._moveto(x, y-0.707106781*self.size_pt),
3978 path._lineto(x, y+0.707106781*self.size_pt))
3980 def square(self, x, y):
3981 return (path._moveto(x-0.5*self.size_pt, y-0.5 * self.size_pt),
3982 path._lineto(x+0.5*self.size_pt, y-0.5 * self.size_pt),
3983 path._lineto(x+0.5*self.size_pt, y+0.5 * self.size_pt),
3984 path._lineto(x-0.5*self.size_pt, y+0.5 * self.size_pt),
3985 path.closepath())
3987 def triangle(self, x, y):
3988 return (path._moveto(x-0.759835685*self.size_pt, y-0.438691337*self.size_pt),
3989 path._lineto(x+0.759835685*self.size_pt, y-0.438691337*self.size_pt),
3990 path._lineto(x, y+0.877382675*self.size_pt),
3991 path.closepath())
3993 def circle(self, x, y):
3994 return (path._arc(x, y, 0.564189583*self.size_pt, 0, 360),
3995 path.closepath())
3997 def diamond(self, x, y):
3998 return (path._moveto(x-0.537284965*self.size_pt, y),
3999 path._lineto(x, y-0.930604859*self.size_pt),
4000 path._lineto(x+0.537284965*self.size_pt, y),
4001 path._lineto(x, y+0.930604859*self.size_pt),
4002 path.closepath())
4004 def __init__(self, symbol=helper.nodefault,
4005 size="0.2 cm", symbolattrs=deco.stroked(),
4006 errorscale=0.5, errorbarattrs=(),
4007 lineattrs=None):
4008 self.size_str = size
4009 if symbol is helper.nodefault:
4010 self._symbol = changesymbol.cross()
4011 else:
4012 self._symbol = symbol
4013 self._symbolattrs = symbolattrs
4014 self.errorscale = errorscale
4015 self._errorbarattrs = errorbarattrs
4016 self._lineattrs = lineattrs
4018 def iteratedict(self):
4019 result = {}
4020 result["symbol"] = _iterateattr(self._symbol)
4021 result["size"] = _iterateattr(self.size_str)
4022 result["symbolattrs"] = _iterateattrs(self._symbolattrs)
4023 result["errorscale"] = _iterateattr(self.errorscale)
4024 result["errorbarattrs"] = _iterateattrs(self._errorbarattrs)
4025 result["lineattrs"] = _iterateattrs(self._lineattrs)
4026 return result
4028 def iterate(self):
4029 return symbol(**self.iteratedict())
4031 def othercolumnkey(self, key, index):
4032 raise ValueError("unsuitable key '%s'" % key)
4034 def setcolumns(self, graph, columns):
4035 def checkpattern(key, index, pattern, iskey, isindex):
4036 if key is not None:
4037 match = pattern.match(key)
4038 if match:
4039 if isindex is not None: raise ValueError("multiple key specification")
4040 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4041 key = None
4042 iskey = match.groups()[0]
4043 isindex = index
4044 return key, iskey, isindex
4046 self.xi = self.xmini = self.xmaxi = None
4047 self.dxi = self.dxmini = self.dxmaxi = None
4048 self.yi = self.ymini = self.ymaxi = None
4049 self.dyi = self.dymini = self.dymaxi = None
4050 self.xkey = self.ykey = None
4051 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4052 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4053 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4054 XMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
4055 YMinPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
4056 XMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
4057 YMaxPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
4058 DXPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4059 DYPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4060 DXMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[0])
4061 DYMinPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)min$" % graph.Names[1])
4062 DXMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[0])
4063 DYMaxPattern = re.compile(r"d(%s([2-9]|[1-9][0-9]+)?)max$" % graph.Names[1])
4064 for key, index in columns.items():
4065 key, self.xkey, self.xi = checkpattern(key, index, XPattern, self.xkey, self.xi)
4066 key, self.ykey, self.yi = checkpattern(key, index, YPattern, self.ykey, self.yi)
4067 key, self.xkey, self.xmini = checkpattern(key, index, XMinPattern, self.xkey, self.xmini)
4068 key, self.ykey, self.ymini = checkpattern(key, index, YMinPattern, self.ykey, self.ymini)
4069 key, self.xkey, self.xmaxi = checkpattern(key, index, XMaxPattern, self.xkey, self.xmaxi)
4070 key, self.ykey, self.ymaxi = checkpattern(key, index, YMaxPattern, self.ykey, self.ymaxi)
4071 key, self.xkey, self.dxi = checkpattern(key, index, DXPattern, self.xkey, self.dxi)
4072 key, self.ykey, self.dyi = checkpattern(key, index, DYPattern, self.ykey, self.dyi)
4073 key, self.xkey, self.dxmini = checkpattern(key, index, DXMinPattern, self.xkey, self.dxmini)
4074 key, self.ykey, self.dymini = checkpattern(key, index, DYMinPattern, self.ykey, self.dymini)
4075 key, self.xkey, self.dxmaxi = checkpattern(key, index, DXMaxPattern, self.xkey, self.dxmaxi)
4076 key, self.ykey, self.dymaxi = checkpattern(key, index, DYMaxPattern, self.ykey, self.dymaxi)
4077 if key is not None:
4078 self.othercolumnkey(key, index)
4079 if None in (self.xkey, self.ykey): raise ValueError("incomplete axis specification")
4080 if (len(filter(None, (self.xmini, self.dxmini, self.dxi))) > 1 or
4081 len(filter(None, (self.ymini, self.dymini, self.dyi))) > 1 or
4082 len(filter(None, (self.xmaxi, self.dxmaxi, self.dxi))) > 1 or
4083 len(filter(None, (self.ymaxi, self.dymaxi, self.dyi))) > 1):
4084 raise ValueError("multiple errorbar definition")
4085 if ((self.xi is None and self.dxi is not None) or
4086 (self.yi is None and self.dyi is not None) or
4087 (self.xi is None and self.dxmini is not None) or
4088 (self.yi is None and self.dymini is not None) or
4089 (self.xi is None and self.dxmaxi is not None) or
4090 (self.yi is None and self.dymaxi is not None)):
4091 raise ValueError("errorbar definition start value missing")
4092 self.xaxis = graph.axes[self.xkey]
4093 self.yaxis = graph.axes[self.ykey]
4095 def minmidmax(self, point, i, mini, maxi, di, dmini, dmaxi):
4096 min = max = mid = None
4097 try:
4098 mid = point[i] + 0.0
4099 except (TypeError, ValueError):
4100 pass
4101 try:
4102 if di is not None: min = point[i] - point[di]
4103 elif dmini is not None: min = point[i] - point[dmini]
4104 elif mini is not None: min = point[mini] + 0.0
4105 except (TypeError, ValueError):
4106 pass
4107 try:
4108 if di is not None: max = point[i] + point[di]
4109 elif dmaxi is not None: max = point[i] + point[dmaxi]
4110 elif maxi is not None: max = point[maxi] + 0.0
4111 except (TypeError, ValueError):
4112 pass
4113 if mid is not None:
4114 if min is not None and min > mid: raise ValueError("minimum error in errorbar")
4115 if max is not None and max < mid: raise ValueError("maximum error in errorbar")
4116 else:
4117 if min is not None and max is not None and min > max: raise ValueError("minimum/maximum error in errorbar")
4118 return min, mid, max
4120 def keyrange(self, points, i, mini, maxi, di, dmini, dmaxi):
4121 allmin = allmax = None
4122 if filter(None, (mini, maxi, di, dmini, dmaxi)) is not None:
4123 for point in points:
4124 min, mid, max = self.minmidmax(point, i, mini, maxi, di, dmini, dmaxi)
4125 if min is not None and (allmin is None or min < allmin): allmin = min
4126 if mid is not None and (allmin is None or mid < allmin): allmin = mid
4127 if mid is not None and (allmax is None or mid > allmax): allmax = mid
4128 if max is not None and (allmax is None or max > allmax): allmax = max
4129 else:
4130 for point in points:
4131 try:
4132 value = point[i] + 0.0
4133 if allmin is None or point[i] < allmin: allmin = point[i]
4134 if allmax is None or point[i] > allmax: allmax = point[i]
4135 except (TypeError, ValueError):
4136 pass
4137 return allmin, allmax
4139 def getranges(self, points):
4140 xmin, xmax = self.keyrange(points, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4141 ymin, ymax = self.keyrange(points, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4142 return {self.xkey: (xmin, xmax), self.ykey: (ymin, ymax)}
4144 def drawerrorbar_pt(self, graph, topleft, top, topright,
4145 left, center, right,
4146 bottomleft, bottom, bottomright, point=None):
4147 if left is not None:
4148 if right is not None:
4149 left1 = graph._addpos(*(left+(0, -self.errorsize_pt)))
4150 left2 = graph._addpos(*(left+(0, self.errorsize_pt)))
4151 right1 = graph._addpos(*(right+(0, -self.errorsize_pt)))
4152 right2 = graph._addpos(*(right+(0, self.errorsize_pt)))
4153 graph.stroke(path.path(path._moveto(*left1),
4154 graph._connect(*(left1+left2)),
4155 path._moveto(*left),
4156 graph._connect(*(left+right)),
4157 path._moveto(*right1),
4158 graph._connect(*(right1+right2))),
4159 *self.errorbarattrs)
4160 elif center is not None:
4161 left1 = graph._addpos(*(left+(0, -self.errorsize_pt)))
4162 left2 = graph._addpos(*(left+(0, self.errorsize_pt)))
4163 graph.stroke(path.path(path._moveto(*left1),
4164 graph._connect(*(left1+left2)),
4165 path._moveto(*left),
4166 graph._connect(*(left+center))),
4167 *self.errorbarattrs)
4168 else:
4169 left1 = graph._addpos(*(left+(0, -self.errorsize_pt)))
4170 left2 = graph._addpos(*(left+(0, self.errorsize_pt)))
4171 left3 = graph._addpos(*(left+(self.errorsize_pt, 0)))
4172 graph.stroke(path.path(path._moveto(*left1),
4173 graph._connect(*(left1+left2)),
4174 path._moveto(*left),
4175 graph._connect(*(left+left3))),
4176 *self.errorbarattrs)
4177 if right is not None and left is None:
4178 if center is not None:
4179 right1 = graph._addpos(*(right+(0, -self.errorsize_pt)))
4180 right2 = graph._addpos(*(right+(0, self.errorsize_pt)))
4181 graph.stroke(path.path(path._moveto(*right1),
4182 graph._connect(*(right1+right2)),
4183 path._moveto(*right),
4184 graph._connect(*(right+center))),
4185 *self.errorbarattrs)
4186 else:
4187 right1 = graph._addpos(*(right+(0, -self.errorsize_pt)))
4188 right2 = graph._addpos(*(right+(0, self.errorsize_pt)))
4189 right3 = graph._addpos(*(right+(-self.errorsize_pt, 0)))
4190 graph.stroke(path.path(path._moveto(*right1),
4191 graph._connect(*(right1+right2)),
4192 path._moveto(*right),
4193 graph._connect(*(right+right3))),
4194 *self.errorbarattrs)
4196 if bottom is not None:
4197 if top is not None:
4198 bottom1 = graph._addpos(*(bottom+(-self.errorsize_pt, 0)))
4199 bottom2 = graph._addpos(*(bottom+(self.errorsize_pt, 0)))
4200 top1 = graph._addpos(*(top+(-self.errorsize_pt, 0)))
4201 top2 = graph._addpos(*(top+(self.errorsize_pt, 0)))
4202 graph.stroke(path.path(path._moveto(*bottom1),
4203 graph._connect(*(bottom1+bottom2)),
4204 path._moveto(*bottom),
4205 graph._connect(*(bottom+top)),
4206 path._moveto(*top1),
4207 graph._connect(*(top1+top2))),
4208 *self.errorbarattrs)
4209 elif center is not None:
4210 bottom1 = graph._addpos(*(bottom+(-self.errorsize_pt, 0)))
4211 bottom2 = graph._addpos(*(bottom+(self.errorsize_pt, 0)))
4212 graph.stroke(path.path(path._moveto(*bottom1),
4213 graph._connect(*(bottom1+bottom2)),
4214 path._moveto(*bottom),
4215 graph._connect(*(bottom+center))),
4216 *self.errorbarattrs)
4217 else:
4218 bottom1 = graph._addpos(*(bottom+(-self.errorsize_pt, 0)))
4219 bottom2 = graph._addpos(*(bottom+(self.errorsize_pt, 0)))
4220 bottom3 = graph._addpos(*(bottom+(0, self.errorsize_pt)))
4221 graph.stroke(path.path(path._moveto(*bottom1),
4222 graph._connect(*(bottom1+bottom2)),
4223 path._moveto(*bottom),
4224 graph._connect(*(bottom+bottom3))),
4225 *self.errorbarattrs)
4226 if top is not None and bottom is None:
4227 if center is not None:
4228 top1 = graph._addpos(*(top+(-self.errorsize_pt, 0)))
4229 top2 = graph._addpos(*(top+(self.errorsize_pt, 0)))
4230 graph.stroke(path.path(path._moveto(*top1),
4231 graph._connect(*(top1+top2)),
4232 path._moveto(*top),
4233 graph._connect(*(top+center))),
4234 *self.errorbarattrs)
4235 else:
4236 top1 = graph._addpos(*(top+(-self.errorsize_pt, 0)))
4237 top2 = graph._addpos(*(top+(self.errorsize_pt, 0)))
4238 top3 = graph._addpos(*(top+(0, -self.errorsize_pt)))
4239 graph.stroke(path.path(path._moveto(*top1),
4240 graph._connect(*(top1+top2)),
4241 path._moveto(*top),
4242 graph._connect(*(top+top3))),
4243 *self.errorbarattrs)
4244 if bottomleft is not None:
4245 if topleft is not None and bottomright is None:
4246 bottomleft1 = graph._addpos(*(bottomleft+(self.errorsize_pt, 0)))
4247 topleft1 = graph._addpos(*(topleft+(self.errorsize_pt, 0)))
4248 graph.stroke(path.path(path._moveto(*bottomleft1),
4249 graph._connect(*(bottomleft1+bottomleft)),
4250 graph._connect(*(bottomleft+topleft)),
4251 graph._connect(*(topleft+topleft1))),
4252 *self.errorbarattrs)
4253 elif bottomright is not None and topleft is None:
4254 bottomleft1 = graph._addpos(*(bottomleft+(0, self.errorsize_pt)))
4255 bottomright1 = graph._addpos(*(bottomright+(0, self.errorsize_pt)))
4256 graph.stroke(path.path(path._moveto(*bottomleft1),
4257 graph._connect(*(bottomleft1+bottomleft)),
4258 graph._connect(*(bottomleft+bottomright)),
4259 graph._connect(*(bottomright+bottomright1))),
4260 *self.errorbarattrs)
4261 elif bottomright is None and topleft is None:
4262 bottomleft1 = graph._addpos(*(bottomleft+(self.errorsize_pt, 0)))
4263 bottomleft2 = graph._addpos(*(bottomleft+(0, self.errorsize_pt)))
4264 graph.stroke(path.path(path._moveto(*bottomleft1),
4265 graph._connect(*(bottomleft1+bottomleft)),
4266 graph._connect(*(bottomleft+bottomleft2))),
4267 *self.errorbarattrs)
4268 if topright is not None:
4269 if bottomright is not None and topleft is None:
4270 topright1 = graph._addpos(*(topright+(-self.errorsize_pt, 0)))
4271 bottomright1 = graph._addpos(*(bottomright+(-self.errorsize_pt, 0)))
4272 graph.stroke(path.path(path._moveto(*topright1),
4273 graph._connect(*(topright1+topright)),
4274 graph._connect(*(topright+bottomright)),
4275 graph._connect(*(bottomright+bottomright1))),
4276 *self.errorbarattrs)
4277 elif topleft is not None and bottomright is None:
4278 topright1 = graph._addpos(*(topright+(0, -self.errorsize_pt)))
4279 topleft1 = graph._addpos(*(topleft+(0, -self.errorsize_pt)))
4280 graph.stroke(path.path(path._moveto(*topright1),
4281 graph._connect(*(topright1+topright)),
4282 graph._connect(*(topright+topleft)),
4283 graph._connect(*(topleft+topleft1))),
4284 *self.errorbarattrs)
4285 elif topleft is None and bottomright is None:
4286 topright1 = graph._addpos(*(topright+(-self.errorsize_pt, 0)))
4287 topright2 = graph._addpos(*(topright+(0, -self.errorsize_pt)))
4288 graph.stroke(path.path(path._moveto(*topright1),
4289 graph._connect(*(topright1+topright)),
4290 graph._connect(*(topright+topright2))),
4291 *self.errorbarattrs)
4292 if bottomright is not None and bottomleft is None and topright is None:
4293 bottomright1 = graph._addpos(*(bottomright+(-self.errorsize_pt, 0)))
4294 bottomright2 = graph._addpos(*(bottomright+(0, self.errorsize_pt)))
4295 graph.stroke(path.path(path._moveto(*bottomright1),
4296 graph._connect(*(bottomright1+bottomright)),
4297 graph._connect(*(bottomright+bottomright2))),
4298 *self.errorbarattrs)
4299 if topleft is not None and bottomleft is None and topright is None:
4300 topleft1 = graph._addpos(*(topleft+(self.errorsize_pt, 0)))
4301 topleft2 = graph._addpos(*(topleft+(0, -self.errorsize_pt)))
4302 graph.stroke(path.path(path._moveto(*topleft1),
4303 graph._connect(*(topleft1+topleft)),
4304 graph._connect(*(topleft+topleft2))),
4305 *self.errorbarattrs)
4306 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4307 graph.stroke(path.path(path._moveto(*bottomleft),
4308 graph._connect(*(bottomleft+bottomright)),
4309 graph._connect(*(bottomright+topright)),
4310 graph._connect(*(topright+topleft)),
4311 path.closepath()),
4312 *self.errorbarattrs)
4314 def drawsymbol_pt(self, canvas, x, y, point=None):
4315 canvas.draw(path.path(*self.symbol(self, x, y)), *self.symbolattrs)
4317 def drawsymbol(self, canvas, x, y, point=None):
4318 self.drawsymbol_pt(canvas, unit.topt(x), unit.topt(y), point)
4320 def key(self, c, x, y, width, height):
4321 if self._symbolattrs is not None:
4322 self.drawsymbol_pt(c, x + 0.5 * width, y + 0.5 * height)
4323 if self._lineattrs is not None:
4324 c.stroke(path._line(x, y + 0.5 * height, x + width, y + 0.5 * height), *self.lineattrs)
4326 def drawpoints(self, graph, points):
4327 xaxismin, xaxismax = self.xaxis.getrange()
4328 yaxismin, yaxismax = self.yaxis.getrange()
4329 self.size = unit.length(_getattr(self.size_str), default_type="v")
4330 self.size_pt = unit.topt(self.size)
4331 self.symbol = _getattr(self._symbol)
4332 self.symbolattrs = _getattrs(helper.ensuresequence(self._symbolattrs))
4333 self.errorbarattrs = _getattrs(helper.ensuresequence(self._errorbarattrs))
4334 self.errorsize_pt = self.errorscale * self.size_pt
4335 self.errorsize = self.errorscale * self.size
4336 self.lineattrs = _getattrs(helper.ensuresequence(self._lineattrs))
4337 if self._lineattrs is not None:
4338 clipcanvas = graph.clipcanvas()
4339 lineels = []
4340 haserror = filter(None, (self.xmini, self.ymini, self.xmaxi, self.ymaxi,
4341 self.dxi, self.dyi, self.dxmini, self.dymini, self.dxmaxi, self.dymaxi)) is not None
4342 moveto = 1
4343 for point in points:
4344 drawsymbol = 1
4345 xmin, x, xmax = self.minmidmax(point, self.xi, self.xmini, self.xmaxi, self.dxi, self.dxmini, self.dxmaxi)
4346 ymin, y, ymax = self.minmidmax(point, self.yi, self.ymini, self.ymaxi, self.dyi, self.dymini, self.dymaxi)
4347 if x is not None and x < xaxismin: drawsymbol = 0
4348 elif x is not None and x > xaxismax: drawsymbol = 0
4349 elif y is not None and y < yaxismin: drawsymbol = 0
4350 elif y is not None and y > yaxismax: drawsymbol = 0
4351 # elif haserror: # TODO: correct clipcanvas handling
4352 # if xmin is not None and xmin < xaxismin: drawsymbol = 0
4353 # elif xmax is not None and xmax < xaxismin: drawsymbol = 0
4354 # elif xmax is not None and xmax > xaxismax: drawsymbol = 0
4355 # elif xmin is not None and xmin > xaxismax: drawsymbol = 0
4356 # elif ymin is not None and ymin < yaxismin: drawsymbol = 0
4357 # elif ymax is not None and ymax < yaxismin: drawsymbol = 0
4358 # elif ymax is not None and ymax > yaxismax: drawsymbol = 0
4359 # elif ymin is not None and ymin > yaxismax: drawsymbol = 0
4360 xpos=ypos=topleft=top=topright=left=center=right=bottomleft=bottom=bottomright=None
4361 if x is not None and y is not None:
4362 try:
4363 center = xpos, ypos = graph.pos_pt(x, y, xaxis=self.xaxis, yaxis=self.yaxis)
4364 except (ValueError, OverflowError): # XXX: exceptions???
4365 pass
4366 if haserror:
4367 if y is not None:
4368 if xmin is not None: left = graph.pos_pt(xmin, y, xaxis=self.xaxis, yaxis=self.yaxis)
4369 if xmax is not None: right = graph.pos_pt(xmax, y, xaxis=self.xaxis, yaxis=self.yaxis)
4370 if x is not None:
4371 if ymax is not None: top = graph.pos_pt(x, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4372 if ymin is not None: bottom = graph.pos_pt(x, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4373 if x is None or y is None:
4374 if ymax is not None:
4375 if xmin is not None: topleft = graph.pos_pt(xmin, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4376 if xmax is not None: topright = graph.pos_pt(xmax, ymax, xaxis=self.xaxis, yaxis=self.yaxis)
4377 if ymin is not None:
4378 if xmin is not None: bottomleft = graph.pos_pt(xmin, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4379 if xmax is not None: bottomright = graph.pos_pt(xmax, ymin, xaxis=self.xaxis, yaxis=self.yaxis)
4380 if drawsymbol:
4381 if self._errorbarattrs is not None and haserror:
4382 self.drawerrorbar_pt(graph, topleft, top, topright,
4383 left, center, right,
4384 bottomleft, bottom, bottomright, point)
4385 if self._symbolattrs is not None and xpos is not None and ypos is not None:
4386 self.drawsymbol_pt(graph, xpos, ypos, point)
4387 if xpos is not None and ypos is not None:
4388 if moveto:
4389 lineels.append(path._moveto(xpos, ypos))
4390 moveto = 0
4391 else:
4392 lineels.append(path._lineto(xpos, ypos))
4393 else:
4394 moveto = 1
4395 self.path = path.path(*lineels)
4396 if self._lineattrs is not None:
4397 clipcanvas.stroke(self.path, *self.lineattrs)
4400 class changesymbol(changesequence): pass
4403 class _changesymbolcross(changesymbol):
4404 defaultsequence = (symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond)
4407 class _changesymbolplus(changesymbol):
4408 defaultsequence = (symbol.plus, symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross)
4411 class _changesymbolsquare(changesymbol):
4412 defaultsequence = (symbol.square, symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus)
4415 class _changesymboltriangle(changesymbol):
4416 defaultsequence = (symbol.triangle, symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square)
4419 class _changesymbolcircle(changesymbol):
4420 defaultsequence = (symbol.circle, symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle)
4423 class _changesymboldiamond(changesymbol):
4424 defaultsequence = (symbol.diamond, symbol.cross, symbol.plus, symbol.square, symbol.triangle, symbol.circle)
4427 class _changesymbolsquaretwice(changesymbol):
4428 defaultsequence = (symbol.square, symbol.square, symbol.triangle, symbol.triangle,
4429 symbol.circle, symbol.circle, symbol.diamond, symbol.diamond)
4432 class _changesymboltriangletwice(changesymbol):
4433 defaultsequence = (symbol.triangle, symbol.triangle, symbol.circle, symbol.circle,
4434 symbol.diamond, symbol.diamond, symbol.square, symbol.square)
4437 class _changesymbolcircletwice(changesymbol):
4438 defaultsequence = (symbol.circle, symbol.circle, symbol.diamond, symbol.diamond,
4439 symbol.square, symbol.square, symbol.triangle, symbol.triangle)
4442 class _changesymboldiamondtwice(changesymbol):
4443 defaultsequence = (symbol.diamond, symbol.diamond, symbol.square, symbol.square,
4444 symbol.triangle, symbol.triangle, symbol.circle, symbol.circle)
4447 changesymbol.cross = _changesymbolcross
4448 changesymbol.plus = _changesymbolplus
4449 changesymbol.square = _changesymbolsquare
4450 changesymbol.triangle = _changesymboltriangle
4451 changesymbol.circle = _changesymbolcircle
4452 changesymbol.diamond = _changesymboldiamond
4453 changesymbol.squaretwice = _changesymbolsquaretwice
4454 changesymbol.triangletwice = _changesymboltriangletwice
4455 changesymbol.circletwice = _changesymbolcircletwice
4456 changesymbol.diamondtwice = _changesymboldiamondtwice
4459 class line(symbol):
4461 def __init__(self, lineattrs=helper.nodefault):
4462 if lineattrs is helper.nodefault:
4463 lineattrs = (changelinestyle(), style.linejoin.round)
4464 symbol.__init__(self, symbolattrs=None, errorbarattrs=None, lineattrs=lineattrs)
4467 class rect(symbol):
4469 def __init__(self, palette=color.palette.Gray):
4470 self.palette = palette
4471 self.colorindex = None
4472 symbol.__init__(self, symbolattrs=None, errorbarattrs=(), lineattrs=None)
4474 def iterate(self):
4475 raise RuntimeError("style is not iterateable")
4477 def othercolumnkey(self, key, index):
4478 if key == "color":
4479 self.colorindex = index
4480 else:
4481 symbol.othercolumnkey(self, key, index)
4483 def drawerrorbar_pt(self, graph, topleft, top, topright,
4484 left, center, right,
4485 bottomleft, bottom, bottomright, point=None):
4486 color = point[self.colorindex]
4487 if color is not None:
4488 if color != self.lastcolor:
4489 self.rectclipcanvas.set(self.palette.getcolor(color))
4490 if bottom is not None and left is not None:
4491 bottomleft = left[0], bottom[1]
4492 if bottom is not None and right is not None:
4493 bottomright = right[0], bottom[1]
4494 if top is not None and right is not None:
4495 topright = right[0], top[1]
4496 if top is not None and left is not None:
4497 topleft = left[0], top[1]
4498 if bottomleft is not None and bottomright is not None and topright is not None and topleft is not None:
4499 self.rectclipcanvas.fill(path.path(path._moveto(*bottomleft),
4500 graph._connect(*(bottomleft+bottomright)),
4501 graph._connect(*(bottomright+topright)),
4502 graph._connect(*(topright+topleft)),
4503 path.closepath()))
4505 def drawpoints(self, graph, points):
4506 if self.colorindex is None:
4507 raise RuntimeError("column 'color' not set")
4508 self.lastcolor = None
4509 self.rectclipcanvas = graph.clipcanvas()
4510 symbol.drawpoints(self, graph, points)
4512 def key(self, c, x, y, width, height):
4513 raise RuntimeError("style doesn't yet provide a key")
4516 class text(symbol):
4518 def __init__(self, textdx="0", textdy="0.3 cm", textattrs=textmodule.halign.center, **args):
4519 self.textindex = None
4520 self.textdx_str = textdx
4521 self.textdy_str = textdy
4522 self._textattrs = textattrs
4523 symbol.__init__(self, **args)
4525 def iteratedict(self):
4526 result = symbol.iteratedict()
4527 result["textattrs"] = _iterateattr(self._textattrs)
4528 return result
4530 def iterate(self):
4531 return textsymbol(**self.iteratedict())
4533 def othercolumnkey(self, key, index):
4534 if key == "text":
4535 self.textindex = index
4536 else:
4537 symbol.othercolumnkey(self, key, index)
4539 def drawsymbol_pt(self, graph, x, y, point=None):
4540 symbol.drawsymbol_pt(self, graph, x, y, point)
4541 if None not in (x, y, point[self.textindex]) and self._textattrs is not None:
4542 graph.text_pt(x + self.textdx_pt, y + self.textdy_pt, str(point[self.textindex]), *helper.ensuresequence(self.textattrs))
4544 def drawpoints(self, graph, points):
4545 self.textdx = unit.length(_getattr(self.textdx_str), default_type="v")
4546 self.textdy = unit.length(_getattr(self.textdy_str), default_type="v")
4547 self.textdx_pt = unit.topt(self.textdx)
4548 self.textdy_pt = unit.topt(self.textdy)
4549 if self._textattrs is not None:
4550 self.textattrs = _getattr(self._textattrs)
4551 if self.textindex is None:
4552 raise RuntimeError("column 'text' not set")
4553 symbol.drawpoints(self, graph, points)
4555 def key(self, c, x, y, width, height):
4556 raise RuntimeError("style doesn't yet provide a key")
4559 class arrow(symbol):
4561 def __init__(self, linelength="0.2 cm", arrowattrs=(), arrowsize="0.1 cm", arrowdict={}, epsilon=1e-10):
4562 self.linelength_str = linelength
4563 self.arrowsize_str = arrowsize
4564 self.arrowattrs = arrowattrs
4565 self.arrowdict = arrowdict
4566 self.epsilon = epsilon
4567 self.sizeindex = self.angleindex = None
4568 symbol.__init__(self, symbolattrs=(), errorbarattrs=None, lineattrs=None)
4570 def iterate(self):
4571 raise RuntimeError("style is not iterateable")
4573 def othercolumnkey(self, key, index):
4574 if key == "size":
4575 self.sizeindex = index
4576 elif key == "angle":
4577 self.angleindex = index
4578 else:
4579 symbol.othercolumnkey(self, key, index)
4581 def drawsymbol_pt(self, graph, x, y, point=None):
4582 if None not in (x, y, point[self.angleindex], point[self.sizeindex], self.arrowattrs, self.arrowdict):
4583 if point[self.sizeindex] > self.epsilon:
4584 dx, dy = math.cos(point[self.angleindex]*math.pi/180.0), math.sin(point[self.angleindex]*math.pi/180)
4585 x1 = unit.t_pt(x)-0.5*dx*self.linelength*point[self.sizeindex]
4586 y1 = unit.t_pt(y)-0.5*dy*self.linelength*point[self.sizeindex]
4587 x2 = unit.t_pt(x)+0.5*dx*self.linelength*point[self.sizeindex]
4588 y2 = unit.t_pt(y)+0.5*dy*self.linelength*point[self.sizeindex]
4589 graph.stroke(path.line(x1, y1, x2, y2),
4590 deco.earrow(self.arrowsize*point[self.sizeindex],
4591 **self.arrowdict),
4592 *helper.ensuresequence(self.arrowattrs))
4594 def drawpoints(self, graph, points):
4595 self.arrowsize = unit.length(_getattr(self.arrowsize_str), default_type="v")
4596 self.linelength = unit.length(_getattr(self.linelength_str), default_type="v")
4597 self.arrowsize_pt = unit.topt(self.arrowsize)
4598 self.linelength_pt = unit.topt(self.linelength)
4599 if self.sizeindex is None:
4600 raise RuntimeError("column 'size' not set")
4601 if self.angleindex is None:
4602 raise RuntimeError("column 'angle' not set")
4603 symbol.drawpoints(self, graph, points)
4605 def key(self, c, x, y, width, height):
4606 raise RuntimeError("style doesn't yet provide a key")
4609 class _bariterator(changeattr):
4611 def attr(self, index):
4612 return index, self.counter
4615 class bar:
4617 def __init__(self, fromzero=1, stacked=0, skipmissing=1, xbar=0,
4618 barattrs=helper.nodefault, _usebariterator=helper.nodefault, _previousbar=None):
4619 self.fromzero = fromzero
4620 self.stacked = stacked
4621 self.skipmissing = skipmissing
4622 self.xbar = xbar
4623 if barattrs is helper.nodefault:
4624 self._barattrs = (deco.stroked(color.gray.black), changecolor.Rainbow())
4625 else:
4626 self._barattrs = barattrs
4627 if _usebariterator is helper.nodefault:
4628 self.bariterator = _bariterator()
4629 else:
4630 self.bariterator = _usebariterator
4631 self.previousbar = _previousbar
4633 def iteratedict(self):
4634 result = {}
4635 result["barattrs"] = _iterateattrs(self._barattrs)
4636 return result
4638 def iterate(self):
4639 return bar(fromzero=self.fromzero, stacked=self.stacked, xbar=self.xbar,
4640 _usebariterator=_iterateattr(self.bariterator), _previousbar=self, **self.iteratedict())
4642 def setcolumns(self, graph, columns):
4643 def checkpattern(key, index, pattern, iskey, isindex):
4644 if key is not None:
4645 match = pattern.match(key)
4646 if match:
4647 if isindex is not None: raise ValueError("multiple key specification")
4648 if iskey is not None and iskey != match.groups()[0]: raise ValueError("inconsistent key names")
4649 key = None
4650 iskey = match.groups()[0]
4651 isindex = index
4652 return key, iskey, isindex
4654 xkey = ykey = None
4655 if len(graph.Names) != 2: raise TypeError("style not applicable in graph")
4656 XPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[0])
4657 YPattern = re.compile(r"(%s([2-9]|[1-9][0-9]+)?)$" % graph.Names[1])
4658 xi = yi = None
4659 for key, index in columns.items():
4660 key, xkey, xi = checkpattern(key, index, XPattern, xkey, xi)
4661 key, ykey, yi = checkpattern(key, index, YPattern, ykey, yi)
4662 if key is not None:
4663 self.othercolumnkey(key, index)
4664 if None in (xkey, ykey): raise ValueError("incomplete axis specification")
4665 if self.xbar:
4666 self.nkey, self.ni = ykey, yi
4667 self.vkey, self.vi = xkey, xi
4668 else:
4669 self.nkey, self.ni = xkey, xi
4670 self.vkey, self.vi = ykey, yi
4671 self.naxis, self.vaxis = graph.axes[self.nkey], graph.axes[self.vkey]
4673 def getranges(self, points):
4674 index, count = _getattr(self.bariterator)
4675 if count != 1 and self.stacked != 1:
4676 if self.stacked > 1:
4677 index = divmod(index, self.stacked)[0]
4679 vmin = vmax = None
4680 for point in points:
4681 if not self.skipmissing:
4682 if count != 1 and self.stacked != 1:
4683 self.naxis.setname(point[self.ni], index)
4684 else:
4685 self.naxis.setname(point[self.ni])
4686 try:
4687 v = point[self.vi] + 0.0
4688 if vmin is None or v < vmin: vmin = v
4689 if vmax is None or v > vmax: vmax = v
4690 except (TypeError, ValueError):
4691 pass
4692 else:
4693 if self.skipmissing:
4694 if count != 1 and self.stacked != 1:
4695 self.naxis.setname(point[self.ni], index)
4696 else:
4697 self.naxis.setname(point[self.ni])
4698 if self.fromzero:
4699 if vmin > 0: vmin = 0
4700 if vmax < 0: vmax = 0
4701 return {self.vkey: (vmin, vmax)}
4703 def drawpoints(self, graph, points):
4704 index, count = _getattr(self.bariterator)
4705 dostacked = (self.stacked != 0 and
4706 (self.stacked == 1 or divmod(index, self.stacked)[1]) and
4707 (self.stacked != 1 or index))
4708 if self.stacked > 1:
4709 index = divmod(index, self.stacked)[0]
4710 vmin, vmax = self.vaxis.getrange()
4711 self.barattrs = _getattrs(helper.ensuresequence(self._barattrs))
4712 if self.stacked:
4713 self.stackedvalue = {}
4714 for point in points:
4715 try:
4716 n = point[self.ni]
4717 v = point[self.vi]
4718 if self.stacked:
4719 self.stackedvalue[n] = v
4720 if count != 1 and self.stacked != 1:
4721 minid = (n, index, 0)
4722 maxid = (n, index, 1)
4723 else:
4724 minid = (n, 0)
4725 maxid = (n, 1)
4726 if self.xbar:
4727 x1pos, y1pos = graph.pos_pt(v, minid, xaxis=self.vaxis, yaxis=self.naxis)
4728 x2pos, y2pos = graph.pos_pt(v, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4729 else:
4730 x1pos, y1pos = graph.pos_pt(minid, v, xaxis=self.naxis, yaxis=self.vaxis)
4731 x2pos, y2pos = graph.pos_pt(maxid, v, xaxis=self.naxis, yaxis=self.vaxis)
4732 if dostacked:
4733 if self.xbar:
4734 x3pos, y3pos = graph.pos_pt(self.previousbar.stackedvalue[n], maxid, xaxis=self.vaxis, yaxis=self.naxis)
4735 x4pos, y4pos = graph.pos_pt(self.previousbar.stackedvalue[n], minid, xaxis=self.vaxis, yaxis=self.naxis)
4736 else:
4737 x3pos, y3pos = graph.pos_pt(maxid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4738 x4pos, y4pos = graph.pos_pt(minid, self.previousbar.stackedvalue[n], xaxis=self.naxis, yaxis=self.vaxis)
4739 else:
4740 if self.fromzero:
4741 if self.xbar:
4742 x3pos, y3pos = graph.pos_pt(0, maxid, xaxis=self.vaxis, yaxis=self.naxis)
4743 x4pos, y4pos = graph.pos_pt(0, minid, xaxis=self.vaxis, yaxis=self.naxis)
4744 else:
4745 x3pos, y3pos = graph.pos_pt(maxid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4746 x4pos, y4pos = graph.pos_pt(minid, 0, xaxis=self.naxis, yaxis=self.vaxis)
4747 else:
4748 #x3pos, y3pos = graph.tickpoint_pt(maxid, axis=self.naxis)
4749 #x4pos, y4pos = graph.tickpoint_pt(minid, axis=self.naxis)
4750 x3pos, y3pos = graph.axespos[self.nkey].tickpoint_pt(maxid)
4751 x4pos, y4pos = graph.axespos[self.nkey].tickpoint_pt(minid)
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)