graph manual finished for 0.6 and small cleanups
[PyX/mjg.git] / pyx / graph / axis / parter.py
blob50c37f8bd1b206b1667e6e6b4b98faf7227b8f4f
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 import math
27 from pyx import helper
28 from pyx.graph.axis import tick
31 # partitioner (parter)
32 # please note the nomenclature:
33 # - a part (partition) is a list of tick instances; thus ticks `==' part
34 # - a parter (partitioner) is a class creating ticks
37 class _Iparter:
38 """interface definition of a partition scheme
39 partition schemes are used to create a list of ticks"""
41 def defaultpart(self, min, max, extendmin, extendmax):
42 """create a partition
43 - returns an ordered list of ticks for the interval min to max
44 - the interval is given in float numbers, thus an appropriate
45 conversion to rational numbers has to be performed
46 - extendmin and extendmax are booleans (integers)
47 - when extendmin or extendmax is set, the ticks might
48 extend the min-max range towards lower and higher
49 ranges, respectively"""
51 def lesspart(self):
52 """create another partition which contains less ticks
53 - this method is called several times after a call of defaultpart
54 - returns an ordered list of ticks with less ticks compared to
55 the partition returned by defaultpart and by previous calls
56 of lesspart
57 - the creation of a partition with strictly *less* ticks
58 is not to be taken serious
59 - the method might return None, when no other appropriate
60 partition can be created"""
63 def morepart(self):
64 """create another partition which contains more ticks
65 see lesspart, but increase the number of ticks"""
68 class linear:
69 """linear partition scheme
70 ticks and label distances are explicitly provided to the constructor"""
72 __implements__ = _Iparter
74 def __init__(self, tickdist=None, labeldist=None, extendtick=0, extendlabel=None, epsilon=1e-10):
75 """configuration of the partition scheme
76 - tickdist and labeldist should be a list, where the first value
77 is the distance between ticks with ticklevel/labellevel 0,
78 the second list for ticklevel/labellevel 1, etc.;
79 a single entry is allowed without being a list
80 - tickdist and labeldist values are passed to the rational constructor
81 - when labeldist is None and tickdist is not None, the tick entries
82 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
83 - extendtick allows for the extension of the range given to the
84 defaultpart method to include the next tick with the specified
85 level (None turns off this feature); note, that this feature is
86 also disabled, when an axis prohibits its range extension by
87 the extendmin/extendmax variables given to the defaultpart method
88 - extendlabel is analogous to extendtick, but for labels
89 - epsilon allows for exceeding the axis range by this relative
90 value (relative to the axis range given to the defaultpart method)
91 without creating another tick specified by extendtick/extendlabel"""
92 if tickdist is None and labeldist is not None:
93 self.ticklist = (tick.rational(helper.ensuresequence(labeldist)[0]),)
94 else:
95 self.ticklist = map(tick.rational, helper.ensuresequence(tickdist))
96 if labeldist is None and tickdist is not None:
97 self.labellist = (tick.rational(helper.ensuresequence(tickdist)[0]),)
98 else:
99 self.labellist = map(tick.rational, helper.ensuresequence(labeldist))
100 self.extendtick = extendtick
101 self.extendlabel = extendlabel
102 self.epsilon = epsilon
104 def extendminmax(self, min, max, dist, extendmin, extendmax):
105 """return new min, max tuple extending the range min, max
106 - dist is the tick distance to be used
107 - extendmin and extendmax are booleans to allow for the extension"""
108 if extendmin:
109 min = float(dist) * math.floor(min / float(dist) + self.epsilon)
110 if extendmax:
111 max = float(dist) * math.ceil(max / float(dist) - self.epsilon)
112 return min, max
114 def getticks(self, min, max, dist, ticklevel=None, labellevel=None):
115 """return a list of equal spaced ticks
116 - the tick distance is dist, the ticklevel is set to ticklevel and
117 the labellevel is set to labellevel
118 - min, max is the range where ticks should be placed"""
119 imin = int(math.ceil(min/float(dist) - 0.5*self.epsilon))
120 imax = int(math.floor(max/float(dist) + 0.5*self.epsilon))
121 ticks = []
122 for i in range(imin, imax + 1):
123 ticks.append(tick.tick((i*dist.enum, dist.denom), ticklevel=ticklevel, labellevel=labellevel))
124 return ticks
126 def defaultpart(self, min, max, extendmin, extendmax):
127 if self.extendtick is not None and len(self.ticklist) > self.extendtick:
128 min, max = self.extendminmax(min, max, self.ticklist[self.extendtick], extendmin, extendmax)
129 if self.extendlabel is not None and len(self.labellist) > self.extendlabel:
130 min, max = self.extendminmax(min, max, self.labellist[self.extendlabel], extendmin, extendmax)
132 ticks = []
133 for i in range(len(self.ticklist)):
134 ticks = tick.mergeticklists(ticks, self.getticks(min, max, self.ticklist[i], ticklevel = i))
135 for i in range(len(self.labellist)):
136 ticks = tick.mergeticklists(ticks, self.getticks(min, max, self.labellist[i], labellevel = i))
138 return ticks
140 def lesspart(self):
141 return None
143 def morepart(self):
144 return None
146 lin = linear
149 class autolinear:
150 """automatic linear partition scheme
151 - possible tick distances are explicitly provided to the constructor
152 - tick distances are adjusted to the axis range by multiplication or division by 10"""
154 __implements__ = _Iparter
156 defaultvariants = [[tick.rational((1, 1)), tick.rational((1, 2))],
157 [tick.rational((2, 1)), tick.rational((1, 1))],
158 [tick.rational((5, 2)), tick.rational((5, 4))],
159 [tick.rational((5, 1)), tick.rational((5, 2))]]
161 def __init__(self, variants=defaultvariants, extendtick=0, epsilon=1e-10):
162 """configuration of the partition scheme
163 - variants is a list of tickdist
164 - tickdist should be a list, where the first value
165 is the distance between ticks with ticklevel 0,
166 the second for ticklevel 1, etc.
167 - tickdist values are passed to the rational constructor
168 - labellevel is set to None except for those ticks in the partitions,
169 where ticklevel is zero. There labellevel is also set to zero.
170 - extendtick allows for the extension of the range given to the
171 defaultpart method to include the next tick with the specified
172 level (None turns off this feature); note, that this feature is
173 also disabled, when an axis prohibits its range extension by
174 the extendmin/extendmax variables given to the defaultpart method
175 - epsilon allows for exceeding the axis range by this relative
176 value (relative to the axis range given to the defaultpart method)
177 without creating another tick specified by extendtick"""
178 self.variants = variants
179 self.extendtick = extendtick
180 self.epsilon = epsilon
182 def defaultpart(self, min, max, extendmin, extendmax):
183 logmm = math.log(max - min) / math.log(10)
184 if logmm < 0: # correction for rounding towards zero of the int routine
185 base = tick.rational((10, 1), power=int(logmm-1))
186 else:
187 base = tick.rational((10, 1), power=int(logmm))
188 ticks = map(tick.rational, self.variants[0])
189 useticks = [t * base for t in ticks]
190 self.lesstickindex = self.moretickindex = 0
191 self.lessbase = tick.rational((base.enum, base.denom))
192 self.morebase = tick.rational((base.enum, base.denom))
193 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
194 part = linear(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
195 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
197 def lesspart(self):
198 if self.lesstickindex < len(self.variants) - 1:
199 self.lesstickindex += 1
200 else:
201 self.lesstickindex = 0
202 self.lessbase.enum *= 10
203 ticks = map(tick.rational, self.variants[self.lesstickindex])
204 useticks = [t * self.lessbase for t in ticks]
205 part = linear(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
206 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
208 def morepart(self):
209 if self.moretickindex:
210 self.moretickindex -= 1
211 else:
212 self.moretickindex = len(self.variants) - 1
213 self.morebase.denom *= 10
214 ticks = map(tick.rational, self.variants[self.moretickindex])
215 useticks = [t * self.morebase for t in ticks]
216 part = linear(tickdist=useticks, extendtick=self.extendtick, epsilon=self.epsilon)
217 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
219 autolin = autolinear
222 class preexp:
223 """storage class for the definition of logarithmic axes partitions
224 instances of this class define tick positions suitable for
225 logarithmic axes by the following instance variables:
226 - exp: integer, which defines multiplicator (usually 10)
227 - pres: list of tick positions (rational numbers, e.g. instances of rational)
228 possible positions are these tick positions and arbitrary divisions
229 and multiplications by the exp value"""
231 def __init__(self, pres, exp):
232 "create a preexp instance and store its pres and exp information"
233 self.pres = pres
234 self.exp = exp
237 class logarithmic(linear):
238 """logarithmic partition scheme
239 ticks and label positions are explicitly provided to the constructor"""
241 __implements__ = _Iparter
243 pre1exp5 = preexp([tick.rational((1, 1))], 100000)
244 pre1exp4 = preexp([tick.rational((1, 1))], 10000)
245 pre1exp3 = preexp([tick.rational((1, 1))], 1000)
246 pre1exp2 = preexp([tick.rational((1, 1))], 100)
247 pre1exp = preexp([tick.rational((1, 1))], 10)
248 pre125exp = preexp([tick.rational((1, 1)), tick.rational((2, 1)), tick.rational((5, 1))], 10)
249 pre1to9exp = preexp([tick.rational((x, 1)) for x in range(1, 10)], 10)
250 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
252 def __init__(self, tickpos=None, labelpos=None, extendtick=0, extendlabel=None, epsilon=1e-10):
253 """configuration of the partition scheme
254 - tickpos and labelpos should be a list, where the first entry
255 is a preexp instance describing ticks with ticklevel/labellevel 0,
256 the second is a preexp instance for ticklevel/labellevel 1, etc.;
257 a single entry is allowed without being a list
258 - when labelpos is None and tickpos is not None, the tick entries
259 for ticklevel 0 are used for labels and vice versa (ticks<->labels)
260 - extendtick allows for the extension of the range given to the
261 defaultpart method to include the next tick with the specified
262 level (None turns off this feature); note, that this feature is
263 also disabled, when an axis prohibits its range extension by
264 the extendmin/extendmax variables given to the defaultpart method
265 - extendlabel is analogous to extendtick, but for labels
266 - epsilon allows for exceeding the axis range by this relative
267 logarithm value (relative to the logarithm axis range given
268 to the defaultpart method) without creating another tick
269 specified by extendtick/extendlabel"""
270 if tickpos is None and labelpos is not None:
271 self.ticklist = (helper.ensuresequence(labelpos)[0],)
272 else:
273 self.ticklist = helper.ensuresequence(tickpos)
275 if labelpos is None and tickpos is not None:
276 self.labellist = (helper.ensuresequence(tickpos)[0],)
277 else:
278 self.labellist = helper.ensuresequence(labelpos)
279 self.extendtick = extendtick
280 self.extendlabel = extendlabel
281 self.epsilon = epsilon
283 def extendminmax(self, min, max, preexp, extendmin, extendmax):
284 """return new min, max tuple extending the range min, max
285 preexp describes the allowed tick positions
286 extendmin and extendmax are booleans to allow for the extension"""
287 minpower = None
288 maxpower = None
289 for i in xrange(len(preexp.pres)):
290 imin = int(math.floor(math.log(min / float(preexp.pres[i])) /
291 math.log(preexp.exp) + self.epsilon)) + 1
292 imax = int(math.ceil(math.log(max / float(preexp.pres[i])) /
293 math.log(preexp.exp) - self.epsilon)) - 1
294 if minpower is None or imin < minpower:
295 minpower, minindex = imin, i
296 if maxpower is None or imax >= maxpower:
297 maxpower, maxindex = imax, i
298 if minindex:
299 minrational = preexp.pres[minindex - 1]
300 else:
301 minrational = preexp.pres[-1]
302 minpower -= 1
303 if maxindex != len(preexp.pres) - 1:
304 maxrational = preexp.pres[maxindex + 1]
305 else:
306 maxrational = preexp.pres[0]
307 maxpower += 1
308 if extendmin:
309 min = float(minrational) * float(preexp.exp) ** minpower
310 if extendmax:
311 max = float(maxrational) * float(preexp.exp) ** maxpower
312 return min, max
314 def getticks(self, min, max, preexp, ticklevel=None, labellevel=None):
315 """return a list of ticks
316 - preexp describes the allowed tick positions
317 - the ticklevel of the ticks is set to ticklevel and
318 the labellevel is set to labellevel
319 - min, max is the range where ticks should be placed"""
320 ticks = []
321 minimin = 0
322 maximax = 0
323 for f in preexp.pres:
324 thisticks = []
325 imin = int(math.ceil(math.log(min / float(f)) /
326 math.log(preexp.exp) - 0.5 * self.epsilon))
327 imax = int(math.floor(math.log(max / float(f)) /
328 math.log(preexp.exp) + 0.5 * self.epsilon))
329 for i in range(imin, imax + 1):
330 pos = f * tick.rational((preexp.exp, 1), power=i)
331 thisticks.append(tick.tick((pos.enum, pos.denom), ticklevel = ticklevel, labellevel = labellevel))
332 ticks = tick.mergeticklists(ticks, thisticks)
333 return ticks
335 log = logarithmic
338 class autologarithmic(logarithmic):
339 """automatic logarithmic partition scheme
340 possible tick positions are explicitly provided to the constructor"""
342 __implements__ = _Iparter
344 defaultvariants = [([logarithmic.pre1exp, # ticks
345 logarithmic.pre1to9exp], # subticks
346 [logarithmic.pre1exp, # labels
347 logarithmic.pre125exp]), # sublevels
349 ([logarithmic.pre1exp, # ticks
350 logarithmic.pre1to9exp], # subticks
351 None), # labels like ticks
353 ([logarithmic.pre1exp2, # ticks
354 logarithmic.pre1exp], # subticks
355 None), # labels like ticks
357 ([logarithmic.pre1exp3, # ticks
358 logarithmic.pre1exp], # subticks
359 None), # labels like ticks
361 ([logarithmic.pre1exp4, # ticks
362 logarithmic.pre1exp], # subticks
363 None), # labels like ticks
365 ([logarithmic.pre1exp5, # ticks
366 logarithmic.pre1exp], # subticks
367 None)] # labels like ticks
369 def __init__(self, variants=defaultvariants, extendtick=0, extendlabel=None, epsilon=1e-10):
370 """configuration of the partition scheme
371 - variants should be a list of pairs of lists of preexp
372 instances
373 - within each pair the first list contains preexp, where
374 the first preexp instance describes ticks positions with
375 ticklevel 0, the second preexp for ticklevel 1, etc.
376 - the second list within each pair describes the same as
377 before, but for labels
378 - within each pair: when the second entry (for the labels) is None
379 and the first entry (for the ticks) ticks is not None, the tick
380 entries for ticklevel 0 are used for labels and vice versa
381 (ticks<->labels)
382 - extendtick allows for the extension of the range given to the
383 defaultpart method to include the next tick with the specified
384 level (None turns off this feature); note, that this feature is
385 also disabled, when an axis prohibits its range extension by
386 the extendmin/extendmax variables given to the defaultpart method
387 - extendlabel is analogous to extendtick, but for labels
388 - epsilon allows for exceeding the axis range by this relative
389 logarithm value (relative to the logarithm axis range given
390 to the defaultpart method) without creating another tick
391 specified by extendtick/extendlabel"""
392 self.variants = variants
393 if len(variants) > 2:
394 self.variantsindex = divmod(len(variants), 2)[0]
395 else:
396 self.variantsindex = 0
397 self.extendtick = extendtick
398 self.extendlabel = extendlabel
399 self.epsilon = epsilon
401 def defaultpart(self, min, max, extendmin, extendmax):
402 self.min, self.max, self.extendmin, self.extendmax = min, max, extendmin, extendmax
403 self.morevariantsindex = self.variantsindex
404 self.lessvariantsindex = self.variantsindex
405 part = logarithmic(tickpos=self.variants[self.variantsindex][0], labelpos=self.variants[self.variantsindex][1],
406 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
407 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
409 def lesspart(self):
410 self.lessvariantsindex += 1
411 if self.lessvariantsindex < len(self.variants):
412 part = logarithmic(tickpos=self.variants[self.lessvariantsindex][0], labelpos=self.variants[self.lessvariantsindex][1],
413 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
414 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
416 def morepart(self):
417 self.morevariantsindex -= 1
418 if self.morevariantsindex >= 0:
419 part = logarithmic(tickpos=self.variants[self.morevariantsindex][0], labelpos=self.variants[self.morevariantsindex][1],
420 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
421 return part.defaultpart(self.min, self.max, self.extendmin, self.extendmax)
423 autolog = autologarithmic