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