1# -*- encoding: utf-8 -*-
2#
3#
4# Copyright (C) 2002-2004 Jörg Lehmann <joerg@pyx-project.org>
5# Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
6# Copyright (C) 2002-2004 André Wobst <wobsta@pyx-project.org>
7#
8# This file is part of PyX (https://pyx-project.org/).
9#
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.
14#
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.
19#
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
23
24from fractions import Fraction
25
26from pyx import text, utils
27from pyx.graph.axis.tick import tick as Tick
28
29
30class _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
42
43
44class decimal(_texter):
45    "a texter creating decimal labels (e.g. '1.234' or even '0.\overline{3}')"
46
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
76
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
133
134            # del tick.temp_decprecision  # we've inserted this temporary variable ... and do not care any longer about it
135
136
137
138class skipmantissaunity:
139    pass
140
141skipmantissaunity.never = 0
142skipmantissaunity.each = 1
143skipmantissaunity.all = 2
144
145
146class default(_texter):
147
148    "a texter creating regular (e.g. '2') and exponential (e.g. '2\cdot10^5') labels"
149
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
190
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'])
196
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)
202
203                tick.labelattrs = tick.labelattrs + self.labelattrs
204
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)
222
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
228
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
238
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
249
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)])
261
262
263class 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
266
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 textengines 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
330
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
350
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
362
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
418
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
422
423