3d function plots
[PyX/mjg.git] / pyx / graph / axis / parter.py
blobf01e73d6f680f04982360e2b9acbc96c82ae8b2e
1 # -*- coding: ISO-8859-1 -*-
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-2006 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 __future__ import nested_scopes
26 import math
27 from pyx.graph.axis import tick
30 # Note: A partition is a list of ticks.
32 class _partdata:
33 """state storage class for a partfunction
35 partdata is used to keep local data and a current state to emulate
36 generators. In the future we might use yield statements within a
37 partfunction. Currently we add partdata by a lambda construct and
38 do inplace modifications within partdata to keep track of the state.
39 """
41 def __init__(self, **kwargs):
42 for key, value in kwargs.items():
43 setattr(self, key, value)
46 class _parter:
47 """interface of a partitioner"""
49 def partfunctions(self, min, max, extendmin, extendmax):
50 """returns a list of partfunctions
52 A partfunction can be called without further arguments and
53 it will return a new partition each time, or None. Several
54 partfunctions are used to walk in different "directions"
55 (like more and less partitions).
57 Note that we do not alternate walking in different directions
58 (i.e. alternate the partfunction calls). Instead we first walk
59 into one direction (which should give less and less ticks) until
60 the rating becomes bad and when try more ticks. We want to keep
61 the number of ticks small compared to a simple alternate search.
62 """
63 # This is a (useless) empty partitioner.
64 return []
67 class linear(_parter):
68 """partitioner to create a single linear parition"""
70 def __init__(self, tickdists=None, labeldists=None, extendtick=0, extendlabel=None, epsilon=1e-10):
71 if tickdists is None and labeldists is not None:
72 self.ticklist = [tick.rational(labeldists[0])]
73 else:
74 self.ticklist = map(tick.rational, tickdists)
75 if labeldists is None and tickdists is not None:
76 self.labellist = [tick.rational(tickdists[0])]
77 else:
78 self.labellist = map(tick.rational, labeldists)
79 self.extendtick = extendtick
80 self.extendlabel = extendlabel
81 self.epsilon = epsilon
83 def extendminmax(self, min, max, dist, extendmin, extendmax):
84 """return new min, max tuple extending the range min, max
85 - dist is the tick distance to be used
86 - extendmin and extendmax are booleans to allow for the extension"""
87 if extendmin:
88 min = float(dist) * math.floor(min / float(dist) + self.epsilon)
89 if extendmax:
90 max = float(dist) * math.ceil(max / float(dist) - self.epsilon)
91 return min, max
93 def getticks(self, min, max, dist, ticklevel=None, labellevel=None):
94 """return a list of equal spaced ticks
95 - the tick distance is dist, the ticklevel is set to ticklevel and
96 the labellevel is set to labellevel
97 - min, max is the range where ticks should be placed"""
98 imin = int(math.ceil(min/float(dist) - 0.5*self.epsilon))
99 imax = int(math.floor(max/float(dist) + 0.5*self.epsilon))
100 ticks = []
101 for i in range(imin, imax + 1):
102 ticks.append(tick.tick((i*dist.num, dist.denom), ticklevel=ticklevel, labellevel=labellevel))
103 return ticks
105 def partfunction(self, data):
106 if data.first:
107 data.first = 0
108 min = data.min
109 max = data.max
110 if self.extendtick is not None and len(self.ticklist) > self.extendtick:
111 min, max = self.extendminmax(min, max, self.ticklist[self.extendtick], data.extendmin, data.extendmax)
112 if self.extendlabel is not None and len(self.labellist) > self.extendlabel:
113 min, max = self.extendminmax(min, max, self.labellist[self.extendlabel], data.extendmin, data.extendmax)
115 ticks = []
116 for i in range(len(self.ticklist)):
117 ticks = tick.mergeticklists(ticks, self.getticks(min, max, self.ticklist[i], ticklevel=i))
118 for i in range(len(self.labellist)):
119 ticks = tick.mergeticklists(ticks, self.getticks(min, max, self.labellist[i], labellevel=i))
121 return ticks
123 return None
125 def partfunctions(self, min, max, extendmin, extendmax):
126 return [lambda d=_partdata(first=1, min=min, max=max, extendmin=extendmin, extendmax=extendmax):
127 self.partfunction(d)]
129 lin = linear
132 class autolinear(_parter):
133 """partitioner to create an arbitrary number of linear paritions"""
135 defaultvariants = [[tick.rational((1, 1)), tick.rational((1, 2))],
136 [tick.rational((2, 1)), tick.rational((1, 1))],
137 [tick.rational((5, 2)), tick.rational((5, 4))],
138 [tick.rational((5, 1)), tick.rational((5, 2))]]
140 def __init__(self, variants=defaultvariants, extendtick=0, epsilon=1e-10):
141 self.variants = variants
142 self.extendtick = extendtick
143 self.epsilon = epsilon
145 def partfunctions(self, min, max, extendmin, extendmax):
146 try:
147 logmm = math.log(max - min) / math.log(10)
148 except ArithmeticError:
149 raise RuntimeError("partitioning failed due to empty or invalid axis range")
150 if logmm < 0: # correction for rounding towards zero of the int routine
151 base = tick.rational((10, 1), power=int(logmm-1))
152 else:
153 base = tick.rational((10, 1), power=int(logmm))
154 ticks = map(tick.rational, self.variants[0])
155 useticks = [t * base for t in ticks]
157 return [lambda d=_partdata(min=min, max=max, extendmin=extendmin, extendmax=extendmax,
158 sign=1, tickindex=-1, base=tick.rational(base)):
159 self.partfunction(d),
160 lambda d=_partdata(min=min, max=max, extendmin=extendmin, extendmax=extendmax,
161 sign=-1, tickindex=0, base=tick.rational(base)):
162 self.partfunction(d)]
164 def partfunction(self, data):
165 if data.sign == 1:
166 if data.tickindex < len(self.variants) - 1:
167 data.tickindex += 1
168 else:
169 data.tickindex = 0
170 data.base.num *= 10
171 else:
172 if data.tickindex:
173 data.tickindex -= 1
174 else:
175 data.tickindex = len(self.variants) - 1
176 data.base.denom *= 10
177 tickdists = [tick.rational(t) * data.base for t in self.variants[data.tickindex]]
178 linearparter = linear(tickdists=tickdists, extendtick=self.extendtick, epsilon=self.epsilon)
179 return linearparter.partfunctions(min=data.min, max=data.max, extendmin=data.extendmin, extendmax=data.extendmax)[0]()
181 autolin = autolinear
184 class preexp:
185 """definition of a logarithmic partition
187 exp is an integer, which defines multiplicator (usually 10).
188 pres are a list of tick positions (rational numbers, e.g.
189 instances of rational). possible positions are the tick
190 positions and arbitrary divisions and multiplications of
191 the tick positions by exp."""
193 def __init__(self, pres, exp):
194 self.pres = pres
195 self.exp = exp
198 class logarithmic(linear):
199 """partitioner to create a single logarithmic parition"""
201 # define some useful constants
202 pre1exp = preexp([tick.rational((1, 1))], 10)
203 pre125exp = preexp([tick.rational((1, 1)), tick.rational((2, 1)), tick.rational((5, 1))], 10)
204 pre1to9exp = preexp([tick.rational((x, 1)) for x in range(1, 10)], 10)
205 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
207 def __init__(self, tickpreexps=None, labelpreexps=None, extendtick=0, extendlabel=None, epsilon=1e-10):
208 if tickpreexps is None and labelpreexps is not None:
209 self.ticklist = [labelpreexps[0]]
210 else:
211 self.ticklist = tickpreexps
213 if labelpreexps is None and tickpreexps is not None:
214 self.labellist = [tickpreexps[0]]
215 else:
216 self.labellist = labelpreexps
217 self.extendtick = extendtick
218 self.extendlabel = extendlabel
219 self.epsilon = epsilon
221 def extendminmax(self, min, max, preexp, extendmin, extendmax):
222 minpower = None
223 maxpower = None
224 for i in xrange(len(preexp.pres)):
225 imin = int(math.floor(math.log(min / float(preexp.pres[i])) /
226 math.log(preexp.exp) + self.epsilon)) + 1
227 imax = int(math.ceil(math.log(max / float(preexp.pres[i])) /
228 math.log(preexp.exp) - self.epsilon)) - 1
229 if minpower is None or imin < minpower:
230 minpower, minindex = imin, i
231 if maxpower is None or imax >= maxpower:
232 maxpower, maxindex = imax, i
233 if minindex:
234 minrational = preexp.pres[minindex - 1]
235 else:
236 minrational = preexp.pres[-1]
237 minpower -= 1
238 if maxindex != len(preexp.pres) - 1:
239 maxrational = preexp.pres[maxindex + 1]
240 else:
241 maxrational = preexp.pres[0]
242 maxpower += 1
243 if extendmin:
244 min = float(minrational) * float(preexp.exp) ** minpower
245 if extendmax:
246 max = float(maxrational) * float(preexp.exp) ** maxpower
247 return min, max
249 def getticks(self, min, max, preexp, ticklevel=None, labellevel=None):
250 ticks = []
251 minimin = 0
252 maximax = 0
253 for f in preexp.pres:
254 thisticks = []
255 imin = int(math.ceil(math.log(min / float(f)) /
256 math.log(preexp.exp) - 0.5 * self.epsilon))
257 imax = int(math.floor(math.log(max / float(f)) /
258 math.log(preexp.exp) + 0.5 * self.epsilon))
259 for i in range(imin, imax + 1):
260 pos = f * tick.rational((preexp.exp, 1), power=i)
261 thisticks.append(tick.tick((pos.num, pos.denom), ticklevel = ticklevel, labellevel = labellevel))
262 ticks = tick.mergeticklists(ticks, thisticks)
263 return ticks
265 log = logarithmic
268 class autologarithmic(logarithmic):
269 """partitioner to create several logarithmic paritions"""
271 defaultvariants = [([logarithmic.pre1exp, # ticks
272 logarithmic.pre1to9exp], # subticks
273 [logarithmic.pre1exp, # labels
274 logarithmic.pre125exp]), # sublevels
276 ([logarithmic.pre1exp, # ticks
277 logarithmic.pre1to9exp], # subticks
278 None)] # labels like ticks
280 def __init__(self, variants=defaultvariants, extendtick=0, extendlabel=None, autoexponent=10, epsilon=1e-10):
281 self.variants = variants
282 self.extendtick = extendtick
283 self.extendlabel = extendlabel
284 self.autoexponent = autoexponent
285 self.epsilon = epsilon
287 def partfunctions(self, min, max, extendmin, extendmax):
288 return [lambda d=_partdata(min=min, max=max, extendmin=extendmin, extendmax=extendmax,
289 variantsindex=len(self.variants)):
290 self.variantspartfunction(d),
291 lambda d=_partdata(min=min, max=max, extendmin=extendmin, extendmax=extendmax,
292 exponent=self.autoexponent):
293 self.autopartfunction(d)]
295 def variantspartfunction(self, data):
296 data.variantsindex -= 1
297 if 0 <= data.variantsindex:
298 logarithmicparter= logarithmic(tickpreexps=self.variants[data.variantsindex][0], labelpreexps=self.variants[data.variantsindex][1],
299 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
300 return logarithmicparter.partfunctions(min=data.min, max=data.max, extendmin=data.extendmin, extendmax=data.extendmax)[0]()
301 return None
303 def autopartfunction(self, data):
304 data.exponent *= self.autoexponent
305 logarithmicparter= logarithmic(tickpreexps=[preexp([tick.rational((1, 1))], data.exponent), logarithmic.pre1exp],
306 extendtick=self.extendtick, extendlabel=self.extendlabel, epsilon=self.epsilon)
307 return logarithmicparter.partfunctions(min=data.min, max=data.max, extendmin=data.extendmin, extendmax=data.extendmax)[0]()
309 autolog = autologarithmic