prevent double call of _cleanup, which harms usefiles (and is a bad idea in general)
[PyX.git] / pyx / graph / axis / tick.py
blobe319b1ad4d01a05c485862426a0d90844746c822
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-2005 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 import sys, functools
27 # test automatic long conversion
28 try:
29 sys.maxsize+1
30 autolong = 1
31 except OverflowError:
32 autolong = 0
35 @functools.total_ordering
36 class rational:
37 """rational class performing some basic rational arithmetics
38 the axis partitioning uses rational arithmetics (with infinite accuracy)
39 basically it contains self.num and self.denom"""
41 def initfromstring(self, s):
42 "converts a string 0.123 into a rational"
43 expparts = s.strip().replace("E", "e").split("e")
44 if len(expparts) > 2:
45 raise ValueError("multiple 'e' found in '%s'" % s)
46 commaparts = expparts[0].split(".")
47 if len(commaparts) > 2:
48 raise ValueError("multiple '.' found in '%s'" % expparts[0])
49 if len(commaparts) == 1:
50 commaparts = [commaparts[0], ""]
51 self.num = 1
52 if autolong:
53 self.denom = 10 ** len(commaparts[1])
54 else:
55 self.denom = 10 ** len(commaparts[1])
56 neg = len(commaparts[0]) and commaparts[0][0] == "-"
57 if neg:
58 commaparts[0] = commaparts[0][1:]
59 elif len(commaparts[0]) and commaparts[0][0] == "+":
60 commaparts[0] = commaparts[0][1:]
61 if len(commaparts[0]):
62 if not commaparts[0].isdigit():
63 raise ValueError("unrecognized characters in '%s'" % s)
64 try:
65 x = int(commaparts[0])
66 except:
67 x = int(commaparts[0])
68 else:
69 x = 0
70 if len(commaparts[1]):
71 if not commaparts[1].isdigit():
72 raise ValueError("unrecognized characters in '%s'" % s)
73 try:
74 y = int(commaparts[1])
75 except:
76 y = int(commaparts[1])
77 else:
78 y = 0
79 self.num = x*self.denom + y
80 if neg:
81 self.num = -self.num
82 if len(expparts) == 2:
83 neg = expparts[1][0] == "-"
84 if neg:
85 expparts[1] = expparts[1][1:]
86 elif expparts[1][0] == "+":
87 expparts[1] = expparts[1][1:]
88 if not expparts[1].isdigit():
89 raise ValueError("unrecognized characters in '%s'" % s)
90 if neg:
91 if autolong:
92 self.denom *= 10 ** int(expparts[1])
93 else:
94 self.denom *= 10 ** int(expparts[1])
95 else:
96 if autolong:
97 self.num *= 10 ** int(expparts[1])
98 else:
99 self.num *= 10 ** int(expparts[1])
101 def initfromfloat(self, x, floatprecision):
102 "converts a float into a rational with finite resolution"
103 if floatprecision < 0:
104 raise RuntimeError("float resolution must be non-negative")
105 self.initfromstring(("%%.%ig" % floatprecision) % x)
107 def __init__(self, x, power=1, floatprecision=10):
108 """initializes a rational
109 - rational=(num/denom)**power
110 - x must be one of:
111 - a string (like "1.2", "1.2e3", "1.2/3.4", etc.)
112 - a float (converted using floatprecision)
113 - a sequence of two integers
114 - a rational instance"""
115 if power == 0:
116 self.num = 1
117 self.denom = 1
118 return
119 try:
120 # does x behave like a number
121 x + 0
122 except:
123 try:
124 # does x behave like a string
125 x + ""
126 except:
127 try:
128 # x might be a tuple
129 self.num, self.denom = x
130 except:
131 # otherwise it should have a num and denom
132 self.num, self.denom = x.num, x.denom
133 else:
134 # x is a string
135 fraction = x.split("/")
136 if len(fraction) > 2:
137 raise ValueError("multiple '/' found in '%s'" % x)
138 self.initfromstring(fraction[0])
139 if len(fraction) == 2:
140 self /= rational(fraction[1])
141 else:
142 # x is a number
143 self.initfromfloat(x, floatprecision)
144 if not self.denom: raise ZeroDivisionError("zero denominator")
145 if power == -1:
146 self.num, self.denom = self.denom, self.num
147 elif power < -1:
148 if autolong:
149 self.num, self.denom = self.denom ** (-power), self.num ** (-power)
150 else:
151 self.num, self.denom = int(self.denom) ** (-power), int(self.num) ** (-power)
152 elif power > 1:
153 if autolong:
154 self.num = self.num ** power
155 self.denom = self.denom ** power
156 else:
157 self.num = int(self.num) ** power
158 self.denom = int(self.denom) ** power
160 def __lt__(self, other):
161 try:
162 return self.num * other.denom < other.num * self.denom
163 except:
164 return float(self) < other
166 def __eq__(self, other):
167 try:
168 return self.num * other.denom == other.num * self.denom
169 except:
170 return float(self) == other
172 def __abs__(self):
173 return rational((abs(self.num), abs(self.denom)))
175 def __add__(self, other):
176 assert abs(other) < 1e-10
177 return float(self)
179 def __mul__(self, other):
180 return rational((self.num * other.num, self.denom * other.denom))
182 def __imul__(self, other):
183 self.num *= other.num
184 self.denom *= other.denom
185 return self
187 def __div__(self, other):
188 return rational((self.num * other.denom, self.denom * other.num))
190 __truediv__ = __div__
192 def __idiv__(self, other):
193 self.num *= other.denom
194 self.denom *= other.num
195 return self
197 __itruediv__ = __idiv__
199 def __float__(self):
200 "caution: avoid final precision of floats"
201 return float(self.num) / self.denom
203 def __str__(self):
204 return "%i/%i" % (self.num, self.denom)
207 class tick(rational):
208 """tick class
209 a tick is a rational enhanced by
210 - self.ticklevel (0 = tick, 1 = subtick, etc.)
211 - self.labellevel (0 = label, 1 = sublabel, etc.)
212 - self.label (a string) and self.labelattrs (a list, defaults to [])
213 When ticklevel or labellevel is None, no tick or label is present at that value.
214 When label is None, it should be automatically created (and stored), once the
215 an axis painter needs it. Classes, which implement _Itexter do precisely that."""
217 def __init__(self, x, ticklevel=0, labellevel=0, label=None, labelattrs=[], **kwargs):
218 """initializes the instance
219 - see class description for the parameter description
220 - **kwargs are passed to the rational constructor"""
221 rational.__init__(self, x, **kwargs)
222 self.ticklevel = ticklevel
223 self.labellevel = labellevel
224 self.label = label
225 self.labelattrs = labelattrs
227 def merge(self, other):
228 """merges two ticks together:
229 - the lower ticklevel/labellevel wins
230 - the ticks should be at the same position (otherwise it doesn't make sense)
231 -> this is NOT checked"""
232 if self.ticklevel is None or (other.ticklevel is not None and other.ticklevel < self.ticklevel):
233 self.ticklevel = other.ticklevel
234 if self.labellevel is None or (other.labellevel is not None and other.labellevel < self.labellevel):
235 self.labellevel = other.labellevel
236 if self.label is None:
237 self.label = other.label
240 def mergeticklists(list1, list2, mergeequal=1):
241 """helper function to merge tick lists
242 - return a merged list of ticks out of list1 and list2
243 - CAUTION: original lists have to be ordered
244 (the returned list is also ordered)"""
245 # TODO: improve along the lines of http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/305269
247 # do not destroy original lists
248 list1 = list1[:]
249 i = 0
250 j = 0
251 try:
252 while True: # we keep on going until we reach an index error
253 while list2[j] < list1[i]: # insert tick
254 list1.insert(i, list2[j])
255 i += 1
256 j += 1
257 if list2[j] == list1[i]: # merge tick
258 if mergeequal:
259 list1[i].merge(list2[j])
260 j += 1
261 i += 1
262 except IndexError:
263 if j < len(list2):
264 list1 += list2[j:]
265 return list1
268 def maxlevels(ticks):
269 "returns a tuple maxticklevel, maxlabellevel from a list of tick instances"
270 maxticklevel = maxlabellevel = 0
271 for tick in ticks:
272 if tick.ticklevel is not None and tick.ticklevel >= maxticklevel:
273 maxticklevel = tick.ticklevel + 1
274 if tick.labellevel is not None and tick.labellevel >= maxlabellevel:
275 maxlabellevel = tick.labellevel + 1
276 return maxticklevel, maxlabellevel