some code reorganisation/renaming: separate graph directory, write(-Postscript) metho...
[PyX/mjg.git] / pyx / graph / texter.py
blobd84c28d54ac4b80e3a31308485a49719463835fc
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26 from pyx import text
27 from pyx.graph import parter
30 class _Itexter:
32 def labels(self, ticks):
33 """fill the label attribute of ticks
34 - ticks is a list of instances of tick
35 - for each element of ticks the value of the attribute label is set to
36 a string appropriate to the attributes enum and denom of that tick
37 instance
38 - label attributes of the tick instances are just kept, whenever they
39 are not equal to None
40 - the method might modify the labelattrs attribute of the ticks; be sure
41 to not modify it in-place!"""
44 class rationaltexter:
45 "a texter creating rational labels (e.g. 'a/b' or even 'a \over b')"
46 # XXX: we use divmod here to be more expicit
48 __implements__ = _Itexter
50 def __init__(self, prefix="", infix="", suffix="",
51 enumprefix="", enuminfix="", enumsuffix="",
52 denomprefix="", denominfix="", denomsuffix="",
53 plus="", minus="-", minuspos=0, over=r"{{%s}\over{%s}}",
54 equaldenom=0, skip1=1, skipenum0=1, skipenum1=1, skipdenom1=1,
55 labelattrs=[text.mathmode]):
56 r"""initializes the instance
57 - prefix, infix, and suffix (strings) are added at the begin,
58 immediately after the minus, and at the end of the label,
59 respectively
60 - prefixenum, infixenum, and suffixenum (strings) are added
61 to the labels enumerator correspondingly
62 - prefixdenom, infixdenom, and suffixdenom (strings) are added
63 to the labels denominator correspondingly
64 - plus or minus (string) is inserted for non-negative or negative numbers
65 - minuspos is an integer, which determines the position, where the
66 plus or minus sign has to be placed; the following values are allowed:
67 1 - writes the plus or minus in front of the enumerator
68 0 - writes the plus or minus in front of the hole fraction
69 -1 - writes the plus or minus in front of the denominator
70 - over (string) is taken as a format string generating the
71 fraction bar; it has to contain exactly two string insert
72 operators "%s" -- the first for the enumerator and the second
73 for the denominator; by far the most common examples are
74 r"{{%s}\over{%s}}" and "{{%s}/{%s}}"
75 - usually the enumerator and denominator are canceled; however,
76 when equaldenom is set, the least common multiple of all
77 denominators is used
78 - skip1 (boolean) just prints the prefix, the plus or minus,
79 the infix and the suffix, when the value is plus or minus one
80 and at least one of prefix, infix and the suffix is present
81 - skipenum0 (boolean) just prints a zero instead of
82 the hole fraction, when the enumerator is zero;
83 no prefixes, infixes, and suffixes are taken into account
84 - skipenum1 (boolean) just prints the enumprefix, the plus or minus,
85 the enuminfix and the enumsuffix, when the enum value is plus or minus one
86 and at least one of enumprefix, enuminfix and the enumsuffix is present
87 - skipdenom1 (boolean) just prints the enumerator instead of
88 the hole fraction, when the denominator is one and none of the parameters
89 denomprefix, denominfix and denomsuffix are set and minuspos is not -1 or the
90 fraction is positive
91 - labelattrs is a list of attributes for a texrunners text method;
92 None is considered as an empty list; labelattrs might be changed
93 in the painter as well"""
94 self.prefix = prefix
95 self.infix = infix
96 self.suffix = suffix
97 self.enumprefix = enumprefix
98 self.enuminfix = enuminfix
99 self.enumsuffix = enumsuffix
100 self.denomprefix = denomprefix
101 self.denominfix = denominfix
102 self.denomsuffix = denomsuffix
103 self.plus = plus
104 self.minus = minus
105 self.minuspos = minuspos
106 self.over = over
107 self.equaldenom = equaldenom
108 self.skip1 = skip1
109 self.skipenum0 = skipenum0
110 self.skipenum1 = skipenum1
111 self.skipdenom1 = skipdenom1
112 self.labelattrs = labelattrs
114 def gcd(self, *n):
115 """returns the greates common divisor of all elements in n
116 - the elements of n must be non-negative integers
117 - return None if the number of elements is zero
118 - the greates common divisor is not affected when some
119 of the elements are zero, but it becomes zero when
120 all elements are zero"""
121 if len(n) == 2:
122 i, j = n
123 if i < j:
124 i, j = j, i
125 while j > 0:
126 i, (dummy, j) = j, divmod(i, j)
127 return i
128 if len(n):
129 res = n[0]
130 for i in n[1:]:
131 res = self.gcd(res, i)
132 return res
134 def lcm(self, *n):
135 """returns the least common multiple of all elements in n
136 - the elements of n must be non-negative integers
137 - return None if the number of elements is zero
138 - the least common multiple is zero when some of the
139 elements are zero"""
140 if len(n):
141 res = n[0]
142 for i in n[1:]:
143 res = divmod(res * i, self.gcd(res, i))[0]
144 return res
146 def labels(self, ticks):
147 labeledticks = []
148 for tick in ticks:
149 if tick.label is None and tick.labellevel is not None:
150 labeledticks.append(tick)
151 tick.temp_fracenum = tick.enum
152 tick.temp_fracdenom = tick.denom
153 tick.temp_fracminus = 1
154 if tick.temp_fracenum < 0:
155 tick.temp_fracminus = -tick.temp_fracminus
156 tick.temp_fracenum = -tick.temp_fracenum
157 if tick.temp_fracdenom < 0:
158 tick.temp_fracminus = -tick.temp_fracminus
159 tick.temp_fracdenom = -tick.temp_fracdenom
160 gcd = self.gcd(tick.temp_fracenum, tick.temp_fracdenom)
161 (tick.temp_fracenum, dummy1), (tick.temp_fracdenom, dummy2) = divmod(tick.temp_fracenum, gcd), divmod(tick.temp_fracdenom, gcd)
162 if self.equaldenom:
163 equaldenom = self.lcm(*[tick.temp_fracdenom for tick in ticks if tick.label is None])
164 if equaldenom is not None:
165 for tick in labeledticks:
166 factor, dummy = divmod(equaldenom, tick.temp_fracdenom)
167 tick.temp_fracenum, tick.temp_fracdenom = factor * tick.temp_fracenum, factor * tick.temp_fracdenom
168 for tick in labeledticks:
169 fracminus = fracenumminus = fracdenomminus = ""
170 if tick.temp_fracminus == -1:
171 plusminus = self.minus
172 else:
173 plusminus = self.plus
174 if self.minuspos == 0:
175 fracminus = plusminus
176 elif self.minuspos == 1:
177 fracenumminus = plusminus
178 elif self.minuspos == -1:
179 fracdenomminus = plusminus
180 else:
181 raise RuntimeError("invalid minuspos")
182 if self.skipenum0 and tick.temp_fracenum == 0:
183 tick.label = "0"
184 elif (self.skip1 and self.skipdenom1 and tick.temp_fracenum == 1 and tick.temp_fracdenom == 1 and
185 (len(self.prefix) or len(self.infix) or len(self.suffix)) and
186 not len(fracenumminus) and not len(self.enumprefix) and not len(self.enuminfix) and not len(self.enumsuffix) and
187 not len(fracdenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix)):
188 tick.label = "%s%s%s%s" % (self.prefix, fracminus, self.infix, self.suffix)
189 else:
190 if self.skipenum1 and tick.temp_fracenum == 1 and (len(self.enumprefix) or len(self.enuminfix) or len(self.enumsuffix)):
191 tick.temp_fracenum = "%s%s%s%s" % (self.enumprefix, fracenumminus, self.enuminfix, self.enumsuffix)
192 else:
193 tick.temp_fracenum = "%s%s%s%i%s" % (self.enumprefix, fracenumminus, self.enuminfix, tick.temp_fracenum, self.enumsuffix)
194 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):
195 frac = tick.temp_fracenum
196 else:
197 tick.temp_fracdenom = "%s%s%s%i%s" % (self.denomprefix, fracdenomminus, self.denominfix, tick.temp_fracdenom, self.denomsuffix)
198 frac = self.over % (tick.temp_fracenum, tick.temp_fracdenom)
199 tick.label = "%s%s%s%s%s" % (self.prefix, fracminus, self.infix, frac, self.suffix)
200 tick.labelattrs = tick.labelattrs + self.labelattrs
202 # del tick.temp_fracenum # we've inserted those temporary variables ... and do not care any longer about them
203 # del tick.temp_fracdenom
204 # del tick.temp_fracminus
208 class decimaltexter:
209 "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')"
211 __implements__ = _Itexter
213 def __init__(self, prefix="", infix="", suffix="", equalprecision=0,
214 decimalsep=".", thousandsep="", thousandthpartsep="",
215 plus="", minus="-", period=r"\overline{%s}",
216 labelattrs=[text.mathmode]):
217 r"""initializes the instance
218 - prefix, infix, and suffix (strings) are added at the begin,
219 immediately after the minus, and at the end of the label,
220 respectively
221 - decimalsep, thousandsep, and thousandthpartsep (strings)
222 are used as separators
223 - plus or minus (string) is inserted for non-negative or negative numbers
224 - period (string) is taken as a format string generating a period;
225 it has to contain exactly one string insert operators "%s" for the
226 period; usually it should be r"\overline{%s}"
227 - labelattrs is a list of attributes for a texrunners text method;
228 a single is allowed without being a list; None is considered as
229 an empty list; labelattrs might be changed in the painter as well"""
230 self.prefix = prefix
231 self.infix = infix
232 self.suffix = suffix
233 self.equalprecision = equalprecision
234 self.decimalsep = decimalsep
235 self.thousandsep = thousandsep
236 self.thousandthpartsep = thousandthpartsep
237 self.plus = plus
238 self.minus = minus
239 self.period = period
240 self.labelattrs = labelattrs
242 def labels(self, ticks):
243 labeledticks = []
244 maxdecprecision = 0
245 for tick in ticks:
246 if tick.label is None and tick.labellevel is not None:
247 labeledticks.append(tick)
248 m, n = tick.enum, tick.denom
249 if m < 0: m = -m
250 if n < 0: n = -n
251 quotient, remainder = divmod(m, n)
252 quotient = str(quotient)
253 if len(self.thousandsep):
254 l = len(quotient)
255 tick.label = ""
256 for i in range(l):
257 tick.label += quotient[i]
258 if not ((l-i-1) % 3) and l > i+1:
259 tick.label += self.thousandsep
260 else:
261 tick.label = quotient
262 if remainder:
263 tick.label += self.decimalsep
264 oldremainders = []
265 tick.temp_decprecision = 0
266 while (remainder):
267 tick.temp_decprecision += 1
268 if remainder in oldremainders:
269 tick.temp_decprecision = None
270 periodstart = len(tick.label) - (len(oldremainders) - oldremainders.index(remainder))
271 tick.label = tick.label[:periodstart] + self.period % tick.label[periodstart:]
272 break
273 oldremainders += [remainder]
274 remainder *= 10
275 quotient, remainder = divmod(remainder, n)
276 if not ((tick.temp_decprecision - 1) % 3) and tick.temp_decprecision > 1:
277 tick.label += self.thousandthpartsep
278 tick.label += str(quotient)
279 if maxdecprecision < tick.temp_decprecision:
280 maxdecprecision = tick.temp_decprecision
281 if self.equalprecision:
282 for tick in labeledticks:
283 if tick.temp_decprecision is not None:
284 if tick.temp_decprecision == 0 and maxdecprecision > 0:
285 tick.label += self.decimalsep
286 for i in range(tick.temp_decprecision, maxdecprecision):
287 if not ((i - 1) % 3) and i > 1:
288 tick.label += self.thousandthpartsep
289 tick.label += "0"
290 for tick in labeledticks:
291 if tick.enum * tick.denom < 0:
292 plusminus = self.minus
293 else:
294 plusminus = self.plus
295 tick.label = "%s%s%s%s%s" % (self.prefix, plusminus, self.infix, tick.label, self.suffix)
296 tick.labelattrs = tick.labelattrs + self.labelattrs
298 # del tick.temp_decprecision # we've inserted this temporary variable ... and do not care any longer about it
301 class exponentialtexter:
302 "a texter creating labels with exponentials (e.g. '2\cdot10^5')"
304 __implements__ = _Itexter
306 def __init__(self, plus="", minus="-",
307 mantissaexp=r"{{%s}\cdot10^{%s}}",
308 skipexp0=r"{%s}",
309 skipexp1=None,
310 nomantissaexp=r"{10^{%s}}",
311 minusnomantissaexp=r"{-10^{%s}}",
312 mantissamin=parter.frac((1, 1)), mantissamax=parter.frac((10, 1)),
313 skipmantissa1=0, skipallmantissa1=1,
314 mantissatexter=decimaltexter()):
315 r"""initializes the instance
316 - plus or minus (string) is inserted for non-negative or negative exponents
317 - mantissaexp (string) is taken as a format string generating the exponent;
318 it has to contain exactly two string insert operators "%s" --
319 the first for the mantissa and the second for the exponent;
320 examples are r"{{%s}\cdot10^{%s}}" and r"{{%s}{\rm e}{%s}}"
321 - skipexp0 (string) is taken as a format string used for exponent 0;
322 exactly one string insert operators "%s" for the mantissa;
323 None turns off the special handling of exponent 0;
324 an example is r"{%s}"
325 - skipexp1 (string) is taken as a format string used for exponent 1;
326 exactly one string insert operators "%s" for the mantissa;
327 None turns off the special handling of exponent 1;
328 an example is r"{{%s}\cdot10}"
329 - nomantissaexp (string) is taken as a format string generating the exponent
330 when the mantissa is one and should be skipped; it has to contain
331 exactly one string insert operators "%s" for the exponent;
332 an examples is r"{10^{%s}}"
333 - minusnomantissaexp (string) is taken as a format string generating the exponent
334 when the mantissa is minus one and should be skipped; it has to contain
335 exactly one string insert operators "%s" for the exponent;
336 None turns off the special handling of mantissa -1;
337 an examples is r"{-10^{%s}}"
338 - mantissamin and mantissamax are the minimum and maximum of the mantissa;
339 they are frac instances greater than zero and mantissamin < mantissamax;
340 the sign of the tick is ignored here
341 - skipmantissa1 (boolean) turns on skipping of any mantissa equals one
342 (and minus when minusnomantissaexp is set)
343 - skipallmantissa1 (boolean) as above, but all mantissas must be 1 (or -1)
344 - mantissatexter is the texter for the mantissa
345 - the skipping of a mantissa is stronger than the skipping of an exponent"""
346 self.plus = plus
347 self.minus = minus
348 self.mantissaexp = mantissaexp
349 self.skipexp0 = skipexp0
350 self.skipexp1 = skipexp1
351 self.nomantissaexp = nomantissaexp
352 self.minusnomantissaexp = minusnomantissaexp
353 self.mantissamin = mantissamin
354 self.mantissamax = mantissamax
355 self.mantissamindivmax = self.mantissamin / self.mantissamax
356 self.mantissamaxdivmin = self.mantissamax / self.mantissamin
357 self.skipmantissa1 = skipmantissa1
358 self.skipallmantissa1 = skipallmantissa1
359 self.mantissatexter = mantissatexter
361 def labels(self, ticks):
362 labeledticks = []
363 for tick in ticks:
364 if tick.label is None and tick.labellevel is not None:
365 tick.temp_orgenum, tick.temp_orgdenom = tick.enum, tick.denom
366 labeledticks.append(tick)
367 tick.temp_exp = 0
368 if tick.enum:
369 while abs(tick) >= self.mantissamax:
370 tick.temp_exp += 1
371 x = tick * self.mantissamindivmax
372 tick.enum, tick.denom = x.enum, x.denom
373 while abs(tick) < self.mantissamin:
374 tick.temp_exp -= 1
375 x = tick * self.mantissamaxdivmin
376 tick.enum, tick.denom = x.enum, x.denom
377 if tick.temp_exp < 0:
378 tick.temp_exp = "%s%i" % (self.minus, -tick.temp_exp)
379 else:
380 tick.temp_exp = "%s%i" % (self.plus, tick.temp_exp)
381 self.mantissatexter.labels(labeledticks)
382 if self.minusnomantissaexp is not None:
383 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if abs(tick.enum) == abs(tick.denom)])
384 else:
385 allmantissa1 = len(labeledticks) == len([tick for tick in labeledticks if tick.enum == tick.denom])
386 for tick in labeledticks:
387 if (self.skipallmantissa1 and allmantissa1 or
388 (self.skipmantissa1 and (tick.enum == tick.denom or
389 (tick.enum == -tick.denom and self.minusnomantissaexp is not None)))):
390 if tick.enum == tick.denom:
391 tick.label = self.nomantissaexp % tick.temp_exp
392 else:
393 tick.label = self.minusnomantissaexp % tick.temp_exp
394 else:
395 if tick.temp_exp == "0" and self.skipexp0 is not None:
396 tick.label = self.skipexp0 % tick.label
397 elif tick.temp_exp == "1" and self.skipexp1 is not None:
398 tick.label = self.skipexp1 % tick.label
399 else:
400 tick.label = self.mantissaexp % (tick.label, tick.temp_exp)
401 tick.enum, tick.denom = tick.temp_orgenum, tick.temp_orgdenom
403 # del tick.temp_orgenum # we've inserted those temporary variables ... and do not care any longer about them
404 # del tick.temp_orgdenom
405 # del tick.temp_exp
408 class defaulttexter:
409 "a texter creating decimal or exponential labels"
411 __implements__ = _Itexter
413 def __init__(self, smallestdecimal=parter.frac((1, 1000)),
414 biggestdecimal=parter.frac((9999, 1)),
415 equaldecision=1,
416 decimaltexter=decimaltexter(),
417 exponentialtexter=exponentialtexter()):
418 """initializes the instance
419 - smallestdecimal and biggestdecimal are the smallest and
420 biggest decimal values, where the decimaltexter should be used;
421 they are frac instances; the sign of the tick is ignored here;
422 a tick at zero is considered for the decimaltexter as well
423 - equaldecision (boolean) uses decimaltexter or exponentialtexter
424 globaly (set) or for each tick separately (unset)
425 - decimaltexter and exponentialtexter are texters to be used"""
426 self.smallestdecimal = smallestdecimal
427 self.biggestdecimal = biggestdecimal
428 self.equaldecision = equaldecision
429 self.decimaltexter = decimaltexter
430 self.exponentialtexter = exponentialtexter
432 def labels(self, ticks):
433 decticks = []
434 expticks = []
435 for tick in ticks:
436 if tick.label is None and tick.labellevel is not None:
437 if not tick.enum or (abs(tick) >= self.smallestdecimal and abs(tick) <= self.biggestdecimal):
438 decticks.append(tick)
439 else:
440 expticks.append(tick)
441 if self.equaldecision:
442 if len(expticks):
443 self.exponentialtexter.labels(ticks)
444 else:
445 self.decimaltexter.labels(ticks)
446 else:
447 for tick in decticks:
448 self.decimaltexter.labels([tick])
449 for tick in expticks:
450 self.exponentialtexter.labels([tick])