add UnicodeEngine (MultiEngineText and axis texters returning MultiEngineText), texte...
[PyX.git] / pyx / graph / axis / texter.py
blobf9fe4c49a43f43a3089dd6145a924b05e79220ef
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
24 from fractions import Fraction
26 from pyx import text, utils
27 from pyx.graph.axis.tick import tick as Tick
30 class _texter:
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 or MultiEngineText instance appropriate to the attributes
36 num and denom of that tick 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!"""
41 raise NotImplementedError
44 class decimal(_texter):
45 "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')"
47 def __init__(self, prefix="", infix="", suffix="", equalprecision=False,
48 decimalsep=".", thousandsep="", thousandthpartsep="",
49 plus="", minus="-", period=r"\overline{%s}",
50 labelattrs=[text.mathmode]):
51 r"""initializes the instance
52 - prefix, infix, and suffix (strings) are added at the begin,
53 immediately after the minus, and at the end of the label,
54 respectively
55 - equalprecision forces the same number of digits after decimalsep,
56 even when the tailing digits are zero
57 - decimalsep, thousandsep, and thousandthpartsep (strings)
58 are used as separators
59 - plus or minus (string) is inserted for non-negative or negative numbers
60 - period (string) is taken as a format string generating a period;
61 it has to contain exactly one string insert operators "%s" for the
62 period; usually it should be r"\overline{%s}"
63 - labelattrs is a list of attributes to be added to the label attributes
64 given in the painter"""
65 self.prefix = prefix
66 self.infix = infix
67 self.suffix = suffix
68 self.equalprecision = equalprecision
69 self.decimalsep = decimalsep
70 self.thousandsep = thousandsep
71 self.thousandthpartsep = thousandthpartsep
72 self.plus = plus
73 self.minus = minus
74 self.period = period
75 self.labelattrs = labelattrs
77 def labels(self, ticks):
78 labeledticks = []
79 maxdecprecision = 0
80 for tick in ticks:
81 if tick.label is None and tick.labellevel is not None:
82 labeledticks.append(tick)
83 m, n = tick.num, tick.denom
84 if m < 0: m = -m
85 if n < 0: n = -n
86 quotient, remainder = divmod(m, n)
87 quotient = str(quotient)
88 if len(self.thousandsep):
89 l = len(quotient)
90 tick.label = ""
91 for i in range(l):
92 tick.label += quotient[i]
93 if not ((l-i-1) % 3) and l > i+1:
94 tick.label += self.thousandsep
95 else:
96 tick.label = quotient
97 if remainder:
98 tick.label += self.decimalsep
99 oldremainders = []
100 tick.temp_decprecision = 0
101 while (remainder):
102 tick.temp_decprecision += 1
103 if remainder in oldremainders:
104 tick.temp_decprecision = None
105 periodstart = len(tick.label) - (len(oldremainders) - oldremainders.index(remainder))
106 tick.label = tick.label[:periodstart] + self.period % tick.label[periodstart:]
107 break
108 oldremainders += [remainder]
109 remainder *= 10
110 quotient, remainder = divmod(remainder, n)
111 if not ((tick.temp_decprecision - 1) % 3) and tick.temp_decprecision > 1:
112 tick.label += self.thousandthpartsep
113 tick.label += str(quotient)
114 else:
115 if maxdecprecision < tick.temp_decprecision:
116 maxdecprecision = tick.temp_decprecision
117 if self.equalprecision:
118 for tick in labeledticks:
119 if tick.temp_decprecision is not None:
120 if tick.temp_decprecision == 0 and maxdecprecision > 0:
121 tick.label += self.decimalsep
122 for i in range(tick.temp_decprecision, maxdecprecision):
123 if not ((i - 1) % 3) and i > 1:
124 tick.label += self.thousandthpartsep
125 tick.label += "0"
126 for tick in labeledticks:
127 if tick.num * tick.denom < 0:
128 plusminus = self.minus
129 else:
130 plusminus = self.plus
131 tick.label = "%s%s%s%s%s" % (self.prefix, plusminus, self.infix, tick.label, self.suffix)
132 tick.labelattrs = tick.labelattrs + self.labelattrs
134 # del tick.temp_decprecision # we've inserted this temporary variable ... and do not care any longer about it
138 class skipmantissaunity:
139 pass
141 skipmantissaunity.never = 0
142 skipmantissaunity.each = 1
143 skipmantissaunity.all = 2
146 class default(_texter):
148 "a texter creating regular (e.g. '2') and exponential (e.g. '2\cdot10^5') labels"
150 def __init__(self, multiplication_tex=r"\cdot{}", multiplication_unicode="·", base=Fraction(10),
151 skipmantissaunity=skipmantissaunity.all, minusunity="-",
152 minexponent=4, minnegexponent=None, uniformexponent=True,
153 mantissatexter=decimal(), basetexter=decimal(), exponenttexter=decimal(),
154 labelattrs=[text.mathmode]):
155 # , **kwargs): # future
156 r"""initializes the instance
157 - multiplication_tex and multiplication_unicode are the strings to
158 indicate the multiplication between the mantissa and the base
159 number for the TexEngine and the UnicodeEngine, respecitvely
160 - base is the number of the base of the exponent
161 - skipmantissaunity is either skipmantissaunity.never (never skip the
162 unity mantissa), skipmantissaunity.each (skip the unity mantissa
163 whenever it occurs for each label separately), or skipmantissaunity.all
164 (skip the unity mantissa whenever if all labels happen to be
165 mantissafixed with unity)
166 - minusunity is used as the output of -unity for the mantissa
167 - minexponent is the minimal positive exponent value to be printed by
168 exponential notation
169 - minnegexponent is the minimal negative exponent value to be printed by
170 exponential notation, for None it is considered to be equal to minexponent
171 - uniformexponent forces all numbers to be written in exponential notation
172 when at least one label excets the limits for non-exponential
173 notiation
174 - mantissatexter, basetexter, and exponenttexter generate the texts
175 for the mantissa, basetexter, and exponenttexter
176 - labelattrs is a list of attributes to be added to the label attributes
177 given in the painter"""
178 self.multiplication_tex = multiplication_tex
179 self.multiplication_unicode = multiplication_unicode
180 self.base = base
181 self.skipmantissaunity = skipmantissaunity
182 self.minusunity = minusunity
183 self.minexponent = minexponent
184 self.minnegexponent = minnegexponent if minnegexponent is not None else minexponent
185 self.uniformexponent = uniformexponent
186 self.mantissatexter = mantissatexter
187 self.basetexter = basetexter
188 self.exponenttexter = exponenttexter
189 self.labelattrs = labelattrs
191 # future:
192 # kwargs = utils.kwsplit(kwargs, ['mantissatexter', 'basetexter', 'exponenttexter'])
193 # self.mantissatexter = mantissatexter(a=1, **kwargs['mantissatexter'])
194 # self.basetexter = basetexter(**kwargs['basetexter'])
195 # self.exponenttexter = exponenttexter(**kwargs['exponenttexter'])
197 def labels(self, ticks):
198 labeledticks = []
199 for tick in ticks:
200 if tick.label is None and tick.labellevel is not None:
201 labeledticks.append(tick)
203 tick.labelattrs = tick.labelattrs + self.labelattrs
205 if tick.num:
206 # express tick = tick.temp_sign * tick.temp_mantissa * self.base ** tick.temp_exponent with 1 <= temp_mantissa < self.base
207 # and decide whether a tick is to be written in exponential notation
208 tick.temp_sign = 1 if tick >= 0 else -1
209 tick.temp_mantissa = abs(Fraction(tick.num, tick.denom))
210 tick.temp_exponent = 0
211 while tick.temp_mantissa >= self.base:
212 tick.temp_exponent += 1
213 tick.temp_mantissa /= self.base
214 while tick.temp_mantissa < 1:
215 tick.temp_exponent -= 1
216 tick.temp_mantissa *= self.base
217 tick.temp_wantexponent = not (-self.minnegexponent < tick.temp_exponent < self.minexponent)
218 else:
219 tick.temp_mantissa = tick.temp_exponent = 0
220 tick.temp_sign = 1
221 tick.temp_wantexponent = not (-self.minnegexponent < 0 < self.minexponent)
223 # make decision on exponential notation uniform if requested
224 if self.uniformexponent and any(tick.temp_wantexponent for tick in labeledticks):
225 for tick in labeledticks:
226 if tick.num:
227 tick.temp_wantexponent = True
229 # mark mantissa == 1 to be not labeled
230 if self.skipmantissaunity == skipmantissaunity.each:
231 for tick in labeledticks:
232 if tick.temp_wantexponent and tick.temp_mantissa == 1:
233 tick.temp_mantissa = None
234 elif self.skipmantissaunity == skipmantissaunity.all and all(tick.temp_mantissa == 1 for tick in labeledticks if tick.temp_wantexponent):
235 for tick in labeledticks:
236 if tick.temp_wantexponent:
237 tick.temp_mantissa = None
239 # construct labels
240 basetick = Tick(self.base, labellevel=0)
241 self.basetexter.labels([basetick])
242 for tick in labeledticks:
243 if tick.temp_wantexponent:
244 if tick.temp_mantissa is not None:
245 tick.temp_mantissatick = Tick(tick.temp_sign * tick.temp_mantissa, labellevel=0)
246 tick.temp_exponenttick = Tick(tick.temp_exponent, labellevel=0)
247 else:
248 tick.temp_mantissatick = tick
250 self.mantissatexter.labels([tick.temp_mantissatick for tick in labeledticks if tick.temp_mantissa is not None])
251 self.exponenttexter.labels([tick.temp_exponenttick for tick in labeledticks if tick.temp_wantexponent])
252 for tick in labeledticks:
253 if tick.temp_wantexponent:
254 if tick.temp_mantissa is not None:
255 mantissalabel_tex = tick.temp_mantissatick.label + self.multiplication_tex
256 mantissalabel_unicode = tick.temp_mantissatick.label + self.multiplication_unicode
257 else:
258 mantissalabel_tex = self.minusunity if tick.temp_sign == -1 else ""
259 mantissalabel_unicode = self.minusunity if tick.temp_sign == -1 else ""
260 tick.label = text.MultiEngineText("%s%s^{%s}" % (mantissalabel_tex, basetick.label, tick.temp_exponenttick.label), [mantissalabel_unicode + basetick.label, text.Text(tick.temp_exponenttick.label, scale=0.8, shift=0.5)])
263 class rational(_texter):
264 "a texter creating rational labels (e.g. 'a/b' or even 'a \over b')"
265 # we use divmod here to be more explicit
267 def __init__(self, prefix="", infix="", suffix="",
268 numprefix="", numinfix="", numsuffix="",
269 denomprefix="", denominfix="", denomsuffix="",
270 plus="", minus="-", minuspos=0, over=r"{{%s}\over{%s}}",
271 equaldenom=False, skip1=True, skipnum0=True, skipnum1=True, skipdenom1=True,
272 labelattrs=[text.mathmode]):
273 r"""initializes the instance
274 - prefix, infix, and suffix (strings) are added at the begin,
275 immediately after the minus, and at the end of the label,
276 respectively
277 - prefixnum, infixnum, and suffixnum (strings) are added
278 to the labels numerator correspondingly
279 - prefixdenom, infixdenom, and suffixdenom (strings) are added
280 to the labels denominator correspondingly
281 - plus or minus (string) is inserted for non-negative or negative numbers
282 - minuspos is an integer, which determines the position, where the
283 plus or minus sign has to be placed; the following values are allowed:
284 1 - writes the plus or minus in front of the numerator
285 0 - writes the plus or minus in front of the hole fraction
286 -1 - writes the plus or minus in front of the denominator
287 - over (string) is taken as a format string generating the
288 fraction bar; it has to contain exactly two string insert
289 operators "%s" -- the first for the numerator and the second
290 for the denominator; by far the most common examples are
291 r"{{%s}\over{%s}}" and "{{%s}/{%s}}"
292 - usually the numerator and denominator are canceled; however,
293 when equaldenom is set, the least common multiple of all
294 denominators is used
295 - skip1 (boolean) just prints the prefix, the plus or minus,
296 the infix and the suffix, when the value is plus or minus one
297 and at least one of prefix, infix and the suffix is present
298 - skipnum0 (boolean) just prints a zero instead of
299 the hole fraction, when the numerator is zero;
300 no prefixes, infixes, and suffixes are taken into account
301 - skipnum1 (boolean) just prints the numprefix, the plus or minus,
302 the numinfix and the numsuffix, when the num value is plus or minus one
303 and at least one of numprefix, numinfix and the numsuffix is present
304 - skipdenom1 (boolean) just prints the numerator instead of
305 the hole fraction, when the denominator is one and none of the parameters
306 denomprefix, denominfix and denomsuffix are set and minuspos is not -1 or the
307 fraction is positive
308 - labelattrs is a list of attributes for a texrunners text method;
309 None is considered as an empty list; labelattrs might be changed
310 in the painter as well"""
311 self.prefix = prefix
312 self.infix = infix
313 self.suffix = suffix
314 self.numprefix = numprefix
315 self.numinfix = numinfix
316 self.numsuffix = numsuffix
317 self.denomprefix = denomprefix
318 self.denominfix = denominfix
319 self.denomsuffix = denomsuffix
320 self.plus = plus
321 self.minus = minus
322 self.minuspos = minuspos
323 self.over = over
324 self.equaldenom = equaldenom
325 self.skip1 = skip1
326 self.skipnum0 = skipnum0
327 self.skipnum1 = skipnum1
328 self.skipdenom1 = skipdenom1
329 self.labelattrs = labelattrs
331 def gcd(self, *n):
332 """returns the greates common divisor of all elements in n
333 - the elements of n must be non-negative integers
334 - return None if the number of elements is zero
335 - the greates common divisor is not affected when some
336 of the elements are zero, but it becomes zero when
337 all elements are zero"""
338 if len(n) == 2:
339 i, j = n
340 if i < j:
341 i, j = j, i
342 while j > 0:
343 i, (dummy, j) = j, divmod(i, j)
344 return i
345 if len(n):
346 res = n[0]
347 for i in n[1:]:
348 res = self.gcd(res, i)
349 return res
351 def lcm(self, *n):
352 """returns the least common multiple of all elements in n
353 - the elements of n must be non-negative integers
354 - return None if the number of elements is zero
355 - the least common multiple is zero when some of the
356 elements are zero"""
357 if len(n):
358 res = n[0]
359 for i in n[1:]:
360 res = divmod(res * i, self.gcd(res, i))[0]
361 return res
363 def labels(self, ticks):
364 labeledticks = []
365 for tick in ticks:
366 if tick.label is None and tick.labellevel is not None:
367 labeledticks.append(tick)
368 tick.temp_rationalnum = tick.num
369 tick.temp_rationaldenom = tick.denom
370 tick.temp_rationalminus = 1
371 if tick.temp_rationalnum < 0:
372 tick.temp_rationalminus = -tick.temp_rationalminus
373 tick.temp_rationalnum = -tick.temp_rationalnum
374 if tick.temp_rationaldenom < 0:
375 tick.temp_rationalminus = -tick.temp_rationalminus
376 tick.temp_rationaldenom = -tick.temp_rationaldenom
377 gcd = self.gcd(tick.temp_rationalnum, tick.temp_rationaldenom)
378 (tick.temp_rationalnum, dummy1), (tick.temp_rationaldenom, dummy2) = divmod(tick.temp_rationalnum, gcd), divmod(tick.temp_rationaldenom, gcd)
379 if self.equaldenom:
380 equaldenom = self.lcm(*[tick.temp_rationaldenom for tick in ticks if tick.label is None])
381 if equaldenom is not None:
382 for tick in labeledticks:
383 factor, dummy = divmod(equaldenom, tick.temp_rationaldenom)
384 tick.temp_rationalnum, tick.temp_rationaldenom = factor * tick.temp_rationalnum, factor * tick.temp_rationaldenom
385 for tick in labeledticks:
386 rationalminus = rationalnumminus = rationaldenomminus = ""
387 if tick.temp_rationalminus == -1:
388 plusminus = self.minus
389 else:
390 plusminus = self.plus
391 if self.minuspos == 0:
392 rationalminus = plusminus
393 elif self.minuspos == 1:
394 rationalnumminus = plusminus
395 elif self.minuspos == -1:
396 rationaldenomminus = plusminus
397 else:
398 raise RuntimeError("invalid minuspos")
399 if self.skipnum0 and tick.temp_rationalnum == 0:
400 tick.label = "0"
401 elif (self.skip1 and self.skipdenom1 and tick.temp_rationalnum == 1 and tick.temp_rationaldenom == 1 and
402 (len(self.prefix) or len(self.infix) or len(self.suffix)) and
403 not len(rationalnumminus) and not len(self.numprefix) and not len(self.numinfix) and not len(self.numsuffix) and
404 not len(rationaldenomminus) and not len(self.denomprefix) and not len(self.denominfix) and not len(self.denomsuffix)):
405 tick.label = "%s%s%s%s" % (self.prefix, rationalminus, self.infix, self.suffix)
406 else:
407 if self.skipnum1 and tick.temp_rationalnum == 1 and (len(self.numprefix) or len(self.numinfix) or len(self.numsuffix)):
408 tick.temp_rationalnum = "%s%s%s%s" % (self.numprefix, rationalnumminus, self.numinfix, self.numsuffix)
409 else:
410 tick.temp_rationalnum = "%s%s%s%i%s" % (self.numprefix, rationalnumminus, self.numinfix, tick.temp_rationalnum, self.numsuffix)
411 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):
412 tick.label = "%s%s%s%s%s" % (self.prefix, rationalminus, self.infix, tick.temp_rationalnum, self.suffix)
413 else:
414 tick.temp_rationaldenom = "%s%s%s%i%s" % (self.denomprefix, rationaldenomminus, self.denominfix, tick.temp_rationaldenom, self.denomsuffix)
415 tick.label = text.MultiEngineText("%s%s%s%s%s" % (self.prefix, rationalminus, self.infix, self.over % (tick.temp_rationalnum, tick.temp_rationaldenom), self.suffix),
416 ["%s%s%s" % (self.prefix, rationalminus, self.infix)] + [text.StackedText([text.Text(tick.temp_rationalnum, shift=0.3), text.Text(tick.temp_rationaldenom, shift=-0.9)], frac=True, align=0.5)] + [self.suffix])
417 tick.labelattrs = tick.labelattrs + self.labelattrs
419 # del tick.temp_rationalnum # we've inserted those temporary variables ... and do not care any longer about them
420 # del tick.temp_rationaldenom
421 # del tick.temp_rationalminus