prevent double call of _cleanup, which harms usefiles (and is a bad idea in general)
[PyX.git] / pyx / graph / axis / texter.py
blobad1a5a69703e166a39edf022ebf0fccc5b35ea82
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2004 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 from pyx import text
26 from pyx.graph.axis import tick
29 class _Itexter:
31 def labels(self, ticks):
32 """fill the label attribute of ticks
33 - ticks is a list of instances of tick
34 - for each element of ticks the value of the attribute label is set to
35 a string appropriate to the attributes num and denom of that tick
36 instance
37 - label attributes of the tick instances are just kept, whenever they
38 are not equal to None
39 - the method might modify the labelattrs attribute of the ticks; be sure
40 to not modify it in-place!"""
43 class decimal:
44 "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')"
46 __implements__ = _Itexter
48 def __init__(self, prefix="", infix="", suffix="", equalprecision=0,
49 decimalsep=".", thousandsep="", thousandthpartsep="",
50 plus="", minus="-", period=r"\overline{%s}",
51 labelattrs=[text.mathmode]):
52 r"""initializes the instance
53 - prefix, infix, and suffix (strings) are added at the begin,
54 immediately after the minus, and at the end of the label,
55 respectively
56 - decimalsep, thousandsep, and thousandthpartsep (strings)
57 are used as separators
58 - plus or minus (string) is inserted for non-negative or negative numbers
59 - period (string) is taken as a format string generating a period;
60 it has to contain exactly one string insert operators "%s" for the
61 period; usually it should be r"\overline{%s}"
62 - labelattrs is a list of attributes to be added to the label attributes
63 given in the painter"""
64 self.prefix = prefix
65 self.infix = infix
66 self.suffix = suffix
67 self.equalprecision = equalprecision
68 self.decimalsep = decimalsep
69 self.thousandsep = thousandsep
70 self.thousandthpartsep = thousandthpartsep
71 self.plus = plus
72 self.minus = minus
73 self.period = period
74 self.labelattrs = labelattrs
76 def labels(self, ticks):
77 labeledticks = []
78 maxdecprecision = 0
79 for tick in ticks:
80 if tick.label is None and tick.labellevel is not None:
81 labeledticks.append(tick)
82 m, n = tick.num, tick.denom
83 if m < 0: m = -m
84 if n < 0: n = -n
85 quotient, remainder = divmod(m, n)
86 quotient = str(quotient)
87 if len(self.thousandsep):
88 l = len(quotient)
89 tick.label = ""
90 for i in range(l):
91 tick.label += quotient[i]
92 if not ((l-i-1) % 3) and l > i+1:
93 tick.label += self.thousandsep
94 else:
95 tick.label = quotient
96 if remainder:
97 tick.label += self.decimalsep
98 oldremainders = []
99 tick.temp_decprecision = 0
100 while (remainder):
101 tick.temp_decprecision += 1
102 if remainder in oldremainders:
103 tick.temp_decprecision = None
104 periodstart = len(tick.label) - (len(oldremainders) - oldremainders.index(remainder))
105 tick.label = tick.label[:periodstart] + self.period % tick.label[periodstart:]
106 break
107 oldremainders += [remainder]
108 remainder *= 10
109 quotient, remainder = divmod(remainder, n)
110 if not ((tick.temp_decprecision - 1) % 3) and tick.temp_decprecision > 1:
111 tick.label += self.thousandthpartsep
112 tick.label += str(quotient)
113 else:
114 if maxdecprecision < tick.temp_decprecision:
115 maxdecprecision = tick.temp_decprecision
116 if self.equalprecision:
117 for tick in labeledticks:
118 if tick.temp_decprecision is not None:
119 if tick.temp_decprecision == 0 and maxdecprecision > 0:
120 tick.label += self.decimalsep
121 for i in range(tick.temp_decprecision, maxdecprecision):
122 if not ((i - 1) % 3) and i > 1:
123 tick.label += self.thousandthpartsep
124 tick.label += "0"
125 for tick in labeledticks:
126 if tick.num * tick.denom < 0:
127 plusminus = self.minus
128 else:
129 plusminus = self.plus
130 tick.label = "%s%s%s%s%s" % (self.prefix, plusminus, self.infix, tick.label, self.suffix)
131 tick.labelattrs = tick.labelattrs + self.labelattrs
133 # del tick.temp_decprecision # we've inserted this temporary variable ... and do not care any longer about it
136 class exponential:
137 "a texter creating labels with exponentials (e.g. '2\cdot10^5')"
139 __implements__ = _Itexter
141 def __init__(self, plus="", minus="-",
142 mantissaexp=r"{{%s}\cdot10^{%s}}",
143 skipexp0=r"{%s}",
144 skipexp1=None,
145 nomantissaexp=r"{10^{%s}}",
146 minusnomantissaexp=r"{-10^{%s}}",
147 mantissamin=tick.rational((1, 1)), mantissamax=tick.rational((10, 1)),
148 skipmantissa1=0, skipallmantissa1=1,
149 mantissatexter=decimal()):
150 r"""initializes the instance
151 - plus or minus (string) is inserted for non-negative or negative exponents
152 - mantissaexp (string) is taken as a format string generating the exponent;
153 it has to contain exactly two string insert operators "%s" --
154 the first for the mantissa and the second for the exponent;
155 examples are r"{{%s}\cdot10^{%s}}" and r"{{%s}{\rm e}{%s}}"
156 - skipexp0 (string) is taken as a format string used for exponent 0;
157 exactly one string insert operators "%s" for the mantissa;
158 None turns off the special handling of exponent 0;
159 an example is r"{%s}"
160 - skipexp1 (string) is taken as a format string used for exponent 1;
161 exactly one string insert operators "%s" for the mantissa;
162 None turns off the special handling of exponent 1;
163 an example is r"{{%s}\cdot10}"
164 - nomantissaexp (string) is taken as a format string generating the exponent
165 when the mantissa is one and should be skipped; it has to contain
166 exactly one string insert operators "%s" for the exponent;
167 an examples is r"{10^{%s}}"
168 - minusnomantissaexp (string) is taken as a format string generating the exponent
169 when the mantissa is minus one and should be skipped; it has to contain
170 exactly one string insert operators "%s" for the exponent;
171 None turns off the special handling of mantissa -1;
172 an examples is r"{-10^{%s}}"
173 - mantissamin and mantissamax are the minimum and maximum of the mantissa;
174 they are rational instances greater than zero and mantissamin < mantissamax;
175 the sign of the tick is ignored here
176 - skipmantissa1 (boolean) turns on skipping of any mantissa equals one
177 (and minus when minusnomantissaexp is set)
178 - skipallmantissa1 (boolean) as above, but all mantissas must be 1 (or -1)
179 - mantissatexter is the texter for the mantissa
180 - the skipping of a mantissa is stronger than the skipping of an exponent"""
181 self.plus = plus
182 self.minus = minus
183 self.mantissaexp = mantissaexp
184 self.skipexp0 = skipexp0
185 self.skipexp1 = skipexp1
186 self.nomantissaexp = nomantissaexp
187 self.minusnomantissaexp = minusnomantissaexp
188 self.mantissamin = mantissamin
189 self.mantissamax = mantissamax
190 self.mantissamindivmax = self.mantissamin / self.mantissamax
191 self.mantissamaxdivmin = self.mantissamax / self.mantissamin
192 self.skipmantissa1 = skipmantissa1
193 self.skipallmantissa1 = skipallmantissa1
194 self.mantissatexter = mantissatexter
196 def labels(self, ticks):
197 labeledticks = []
198 for tick in ticks:
199 if tick.label is None and tick.labellevel is not None:
200 tick.temp_orgnum, tick.temp_orgdenom = tick.num, tick.denom
201 labeledticks.append(tick)
202 tick.temp_exp = 0
203 if tick.num:
204 while abs(tick) >= self.mantissamax:
205 tick.temp_exp += 1
206 x = tick * self.mantissamindivmax
207 tick.num, tick.denom = x.num, x.denom
208 while abs(tick) < self.mantissamin:
209 tick.temp_exp -= 1
210 x = tick * self.mantissamaxdivmin
211 tick.num, tick.denom = x.num, x.denom
212 if tick.temp_exp < 0:
213 tick.temp_exp = "%s%i" % (self.minus, -tick.temp_exp)
214 else:
215 tick.temp_exp = "%s%i" % (self.plus, tick.temp_exp)
216 self.mantissatexter.labels(labeledticks)
217 if self.minusnomantissaexp is not None:
218 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if abs(tick.num) == abs(tick.denom)])
219 else:
220 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if tick.num == tick.denom])
221 for tick in labeledticks:
222 if (self.skipallmantissa1 and allmantissa1 or
223 (self.skipmantissa1 and (tick.num == tick.denom or
224 (tick.num == -tick.denom and self.minusnomantissaexp is not None)))):
225 if tick.num == tick.denom:
226 tick.label = self.nomantissaexp % tick.temp_exp
227 else:
228 tick.label = self.minusnomantissaexp % tick.temp_exp
229 else:
230 if tick.temp_exp == "0" and self.skipexp0 is not None:
231 tick.label = self.skipexp0 % tick.label
232 elif tick.temp_exp == "1" and self.skipexp1 is not None:
233 tick.label = self.skipexp1 % tick.label
234 else:
235 tick.label = self.mantissaexp % (tick.label, tick.temp_exp)
236 tick.num, tick.denom = tick.temp_orgnum, tick.temp_orgdenom
238 # del tick.temp_orgnum # we've inserted those temporary variables ... and do not care any longer about them
239 # del tick.temp_orgdenom
240 # del tick.temp_exp
243 class mixed:
244 "a texter creating decimal or exponential labels"
246 __implements__ = _Itexter
248 def __init__(self, smallestdecimal=tick.rational((1, 1000)),
249 biggestdecimal=tick.rational((9999, 1)),
250 equaldecision=1,
251 decimal=decimal(),
252 exponential=exponential()):
253 """initializes the instance
254 - smallestdecimal and biggestdecimal are the smallest and
255 biggest decimal values, where the decimal texter should be used;
256 they are rational instances; the sign of the tick is ignored here;
257 a tick at zero is considered for the decimal texter as well
258 - equaldecision (boolean) uses decimal texter or exponential texter
259 globaly (set) or for each tick separately (unset)
260 - decimal and exponential are texters to be used"""
261 self.smallestdecimal = smallestdecimal
262 self.biggestdecimal = biggestdecimal
263 self.equaldecision = equaldecision
264 self.decimal = decimal
265 self.exponential = exponential
267 def labels(self, ticks):
268 decticks = []
269 expticks = []
270 for tick in ticks:
271 if tick.label is None and tick.labellevel is not None:
272 if not tick.num or (abs(tick) >= self.smallestdecimal and abs(tick) <= self.biggestdecimal):
273 decticks.append(tick)
274 else:
275 expticks.append(tick)
276 if self.equaldecision:
277 if len(expticks):
278 self.exponential.labels(ticks)
279 else:
280 self.decimal.labels(ticks)
281 else:
282 for tick in decticks:
283 self.decimal.labels([tick])
284 for tick in expticks:
285 self.exponential.labels([tick])
288 class rational:
289 "a texter creating rational labels (e.g. 'a/b' or even 'a \over b')"
290 # XXX: we use divmod here to be more expicit
292 __implements__ = _Itexter
294 def __init__(self, prefix="", infix="", suffix="",
295 numprefix="", numinfix="", numsuffix="",
296 denomprefix="", denominfix="", denomsuffix="",
297 plus="", minus="-", minuspos=0, over=r"{{%s}\over{%s}}",
298 equaldenom=0, skip1=1, skipnum0=1, skipnum1=1, skipdenom1=1,
299 labelattrs=[text.mathmode]):
300 r"""initializes the instance
301 - prefix, infix, and suffix (strings) are added at the begin,
302 immediately after the minus, and at the end of the label,
303 respectively
304 - prefixnum, infixnum, and suffixnum (strings) are added
305 to the labels numerator correspondingly
306 - prefixdenom, infixdenom, and suffixdenom (strings) are added
307 to the labels denominator correspondingly
308 - plus or minus (string) is inserted for non-negative or negative numbers
309 - minuspos is an integer, which determines the position, where the
310 plus or minus sign has to be placed; the following values are allowed:
311 1 - writes the plus or minus in front of the numerator
312 0 - writes the plus or minus in front of the hole fraction
313 -1 - writes the plus or minus in front of the denominator
314 - over (string) is taken as a format string generating the
315 fraction bar; it has to contain exactly two string insert
316 operators "%s" -- the first for the numerator and the second
317 for the denominator; by far the most common examples are
318 r"{{%s}\over{%s}}" and "{{%s}/{%s}}"
319 - usually the numerator and denominator are canceled; however,
320 when equaldenom is set, the least common multiple of all
321 denominators is used
322 - skip1 (boolean) just prints the prefix, the plus or minus,
323 the infix and the suffix, when the value is plus or minus one
324 and at least one of prefix, infix and the suffix is present
325 - skipnum0 (boolean) just prints a zero instead of
326 the hole fraction, when the numerator is zero;
327 no prefixes, infixes, and suffixes are taken into account
328 - skipnum1 (boolean) just prints the numprefix, the plus or minus,
329 the numinfix and the numsuffix, when the num value is plus or minus one
330 and at least one of numprefix, numinfix and the numsuffix is present
331 - skipdenom1 (boolean) just prints the numerator instead of
332 the hole fraction, when the denominator is one and none of the parameters
333 denomprefix, denominfix and denomsuffix are set and minuspos is not -1 or the
334 fraction is positive
335 - labelattrs is a list of attributes for a texrunners text method;
336 None is considered as an empty list; labelattrs might be changed
337 in the painter as well"""
338 self.prefix = prefix
339 self.infix = infix
340 self.suffix = suffix
341 self.numprefix = numprefix
342 self.numinfix = numinfix
343 self.numsuffix = numsuffix
344 self.denomprefix = denomprefix
345 self.denominfix = denominfix
346 self.denomsuffix = denomsuffix
347 self.plus = plus
348 self.minus = minus
349 self.minuspos = minuspos
350 self.over = over
351 self.equaldenom = equaldenom
352 self.skip1 = skip1
353 self.skipnum0 = skipnum0
354 self.skipnum1 = skipnum1
355 self.skipdenom1 = skipdenom1
356 self.labelattrs = labelattrs
358 def gcd(self, *n):
359 """returns the greates common divisor of all elements in n
360 - the elements of n must be non-negative integers
361 - return None if the number of elements is zero
362 - the greates common divisor is not affected when some
363 of the elements are zero, but it becomes zero when
364 all elements are zero"""
365 if len(n) == 2:
366 i, j = n
367 if i < j:
368 i, j = j, i
369 while j > 0:
370 i, (dummy, j) = j, divmod(i, j)
371 return i
372 if len(n):
373 res = n[0]
374 for i in n[1:]:
375 res = self.gcd(res, i)
376 return res
378 def lcm(self, *n):
379 """returns the least common multiple of all elements in n
380 - the elements of n must be non-negative integers
381 - return None if the number of elements is zero
382 - the least common multiple is zero when some of the
383 elements are zero"""
384 if len(n):
385 res = n[0]
386 for i in n[1:]:
387 res = divmod(res * i, self.gcd(res, i))[0]
388 return res
390 def labels(self, ticks):
391 labeledticks = []
392 for tick in ticks:
393 if tick.label is None and tick.labellevel is not None:
394 labeledticks.append(tick)
395 tick.temp_rationalnum = tick.num
396 tick.temp_rationaldenom = tick.denom
397 tick.temp_rationalminus = 1
398 if tick.temp_rationalnum < 0:
399 tick.temp_rationalminus = -tick.temp_rationalminus
400 tick.temp_rationalnum = -tick.temp_rationalnum
401 if tick.temp_rationaldenom < 0:
402 tick.temp_rationalminus = -tick.temp_rationalminus
403 tick.temp_rationaldenom = -tick.temp_rationaldenom
404 gcd = self.gcd(tick.temp_rationalnum, tick.temp_rationaldenom)
405 (tick.temp_rationalnum, dummy1), (tick.temp_rationaldenom, dummy2) = divmod(tick.temp_rationalnum, gcd), divmod(tick.temp_rationaldenom, gcd)
406 if self.equaldenom:
407 equaldenom = self.lcm(*[tick.temp_rationaldenom for tick in ticks if tick.label is None])
408 if equaldenom is not None:
409 for tick in labeledticks:
410 factor, dummy = divmod(equaldenom, tick.temp_rationaldenom)
411 tick.temp_rationalnum, tick.temp_rationaldenom = factor * tick.temp_rationalnum, factor * tick.temp_rationaldenom
412 for tick in labeledticks:
413 rationalminus = rationalnumminus = rationaldenomminus = ""
414 if tick.temp_rationalminus == -1:
415 plusminus = self.minus
416 else:
417 plusminus = self.plus
418 if self.minuspos == 0:
419 rationalminus = plusminus
420 elif self.minuspos == 1:
421 rationalnumminus = plusminus
422 elif self.minuspos == -1:
423 rationaldenomminus = plusminus
424 else:
425 raise RuntimeError("invalid minuspos")
426 if self.skipnum0 and tick.temp_rationalnum == 0:
427 tick.label = "0"
428 elif (self.skip1 and self.skipdenom1 and tick.temp_rationalnum == 1 and tick.temp_rationaldenom == 1 and
429 (len(self.prefix) or len(self.infix) or len(self.suffix)) and
430 not len(rationalnumminus) and not len(self.numprefix) and not len(self.numinfix) and not len(self.numsuffix) and
431 not len(rationaldenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix)):
432 tick.label = "%s%s%s%s" % (self.prefix, rationalminus, self.infix, self.suffix)
433 else:
434 if self.skipnum1 and tick.temp_rationalnum == 1 and (len(self.numprefix) or len(self.numinfix) or len(self.numsuffix)):
435 tick.temp_rationalnum = "%s%s%s%s" % (self.numprefix, rationalnumminus, self.numinfix, self.numsuffix)
436 else:
437 tick.temp_rationalnum = "%s%s%s%i%s" % (self.numprefix, rationalnumminus, self.numinfix, tick.temp_rationalnum, self.numsuffix)
438 if self.skipdenom1 and tick.temp_rationaldenom == 1 and not len(rationaldenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix):
439 rational = tick.temp_rationalnum
440 else:
441 tick.temp_rationaldenom = "%s%s%s%i%s" % (self.denomprefix, rationaldenomminus, self.denominfix, tick.temp_rationaldenom, self.denomsuffix)
442 rational = self.over % (tick.temp_rationalnum, tick.temp_rationaldenom)
443 tick.label = "%s%s%s%s%s" % (self.prefix, rationalminus, self.infix, rational, self.suffix)
444 tick.labelattrs = tick.labelattrs + self.labelattrs
446 # del tick.temp_rationalnum # we've inserted those temporary variables ... and do not care any longer about them
447 # del tick.temp_rationaldenom
448 # del tick.temp_rationalminus