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-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
25 from pyx
.graph
.axis
import tick
28 # Note: A partition is a list of ticks.
31 """state storage class for a partfunction
33 partdata is used to keep local data and a current state to emulate
34 generators. In the future we might use yield statements within a
35 partfunction. Currently we add partdata by a lambda construct and
36 do inplace modifications within partdata to keep track of the state.
39 def __init__(self
, **kwargs
):
40 for key
, value
in list(kwargs
.items()):
41 setattr(self
, key
, value
)
45 """interface of a partitioner"""
47 def partfunctions(self
, min, max, extendmin
, extendmax
):
48 """returns a list of partfunctions
50 A partfunction can be called without further arguments and
51 it will return a new partition each time, or None. Several
52 partfunctions are used to walk in different "directions"
53 (like more and less partitions).
55 Note that we do not alternate walking in different directions
56 (i.e. alternate the partfunction calls). Instead we first walk
57 into one direction (which should give less and less ticks) until
58 the rating becomes bad and when try more ticks. We want to keep
59 the number of ticks small compared to a simple alternate search.
61 # This is a (useless) empty partitioner.
65 class linear(_parter
):
66 """partitioner to create a single linear partition"""
68 def __init__(self
, tickdists
=None, labeldists
=None, extendtick
=0, extendlabel
=None, epsilon
=1e-10):
69 if tickdists
is None and labeldists
is not None:
70 self
.ticklist
= [tick
.rational(labeldists
[0])]
72 self
.ticklist
= list(map(tick
.rational
, tickdists
))
73 if labeldists
is None and tickdists
is not None:
74 self
.labellist
= [tick
.rational(tickdists
[0])]
76 self
.labellist
= list(map(tick
.rational
, labeldists
))
77 self
.extendtick
= extendtick
78 self
.extendlabel
= extendlabel
79 self
.epsilon
= epsilon
81 def extendminmax(self
, min, max, dist
, extendmin
, extendmax
):
82 """return new min, max tuple extending the range min, max
83 - dist is the tick distance to be used
84 - extendmin and extendmax are booleans to allow for the extension"""
86 min = float(dist
) * math
.floor(min / float(dist
) + self
.epsilon
)
88 max = float(dist
) * math
.ceil(max / float(dist
) - self
.epsilon
)
91 def getticks(self
, min, max, dist
, ticklevel
=None, labellevel
=None):
92 """return a list of equal spaced ticks
93 - the tick distance is dist, the ticklevel is set to ticklevel and
94 the labellevel is set to labellevel
95 - min, max is the range where ticks should be placed"""
96 imin
= int(math
.ceil(min/float(dist
) - 0.5*self
.epsilon
))
97 imax
= int(math
.floor(max/float(dist
) + 0.5*self
.epsilon
))
99 for i
in range(imin
, imax
+ 1):
100 ticks
.append(tick
.tick((i
*dist
.num
, dist
.denom
), ticklevel
=ticklevel
, labellevel
=labellevel
))
103 def partfunction(self
, data
):
108 if self
.extendtick
is not None and len(self
.ticklist
) > self
.extendtick
:
109 min, max = self
.extendminmax(min, max, self
.ticklist
[self
.extendtick
], data
.extendmin
, data
.extendmax
)
110 if self
.extendlabel
is not None and len(self
.labellist
) > self
.extendlabel
:
111 min, max = self
.extendminmax(min, max, self
.labellist
[self
.extendlabel
], data
.extendmin
, data
.extendmax
)
114 for i
in range(len(self
.ticklist
)):
115 ticks
= tick
.mergeticklists(ticks
, self
.getticks(min, max, self
.ticklist
[i
], ticklevel
=i
))
116 for i
in range(len(self
.labellist
)):
117 ticks
= tick
.mergeticklists(ticks
, self
.getticks(min, max, self
.labellist
[i
], labellevel
=i
))
123 def partfunctions(self
, min, max, extendmin
, extendmax
):
124 return [lambda d
=_partdata(first
=1, min=min, max=max, extendmin
=extendmin
, extendmax
=extendmax
):
125 self
.partfunction(d
)]
130 class autolinear(_parter
):
131 """partitioner to create an arbitrary number of linear partitions"""
133 defaultvariants
= [[tick
.rational((1, 1)), tick
.rational((1, 2))],
134 [tick
.rational((2, 1)), tick
.rational((1, 1))],
135 [tick
.rational((5, 2)), tick
.rational((5, 4))],
136 [tick
.rational((5, 1)), tick
.rational((5, 2))]]
138 def __init__(self
, variants
=defaultvariants
, extendtick
=0, epsilon
=1e-10):
139 self
.variants
= variants
140 self
.extendtick
= extendtick
141 self
.epsilon
= epsilon
143 def partfunctions(self
, min, max, extendmin
, extendmax
):
145 logmm
= math
.log(max - min) / math
.log(10)
146 except ArithmeticError:
147 raise RuntimeError("partitioning failed due to empty or invalid axis range")
148 if logmm
< 0: # correction for rounding towards zero of the int routine
149 base
= tick
.rational((10, 1), power
=int(logmm
-1))
151 base
= tick
.rational((10, 1), power
=int(logmm
))
152 ticks
= list(map(tick
.rational
, self
.variants
[0]))
153 useticks
= [t
* base
for t
in ticks
]
155 return [lambda d
=_partdata(min=min, max=max, extendmin
=extendmin
, extendmax
=extendmax
,
156 sign
=1, tickindex
=-1, base
=tick
.rational(base
)):
157 self
.partfunction(d
),
158 lambda d
=_partdata(min=min, max=max, extendmin
=extendmin
, extendmax
=extendmax
,
159 sign
=-1, tickindex
=0, base
=tick
.rational(base
)):
160 self
.partfunction(d
)]
162 def partfunction(self
, data
):
164 if data
.tickindex
< len(self
.variants
) - 1:
173 data
.tickindex
= len(self
.variants
) - 1
174 data
.base
.denom
*= 10
175 tickdists
= [tick
.rational(t
) * data
.base
for t
in self
.variants
[data
.tickindex
]]
176 linearparter
= linear(tickdists
=tickdists
, extendtick
=self
.extendtick
, epsilon
=self
.epsilon
)
177 return linearparter
.partfunctions(min=data
.min, max=data
.max, extendmin
=data
.extendmin
, extendmax
=data
.extendmax
)[0]()
183 """definition of a logarithmic partition
185 exp is an integer, which defines multiplicator (usually 10).
186 pres are a list of tick positions (rational numbers, e.g.
187 instances of rational). possible positions are the tick
188 positions and arbitrary divisions and multiplications of
189 the tick positions by exp."""
191 def __init__(self
, pres
, exp
):
196 class logarithmic(linear
):
197 """partitioner to create a single logarithmic partition"""
199 # define some useful constants
200 pre1exp
= preexp([tick
.rational((1, 1))], 10)
201 pre125exp
= preexp([tick
.rational((1, 1)), tick
.rational((2, 1)), tick
.rational((5, 1))], 10)
202 pre1to9exp
= preexp([tick
.rational((x
, 1)) for x
in range(1, 10)], 10)
203 # ^- we always include 1 in order to get extendto(tick|label)level to work as expected
205 def __init__(self
, tickpreexps
=None, labelpreexps
=None, extendtick
=0, extendlabel
=None, epsilon
=1e-10):
206 if tickpreexps
is None and labelpreexps
is not None:
207 self
.ticklist
= [labelpreexps
[0]]
209 self
.ticklist
= tickpreexps
211 if labelpreexps
is None and tickpreexps
is not None:
212 self
.labellist
= [tickpreexps
[0]]
214 self
.labellist
= labelpreexps
215 self
.extendtick
= extendtick
216 self
.extendlabel
= extendlabel
217 self
.epsilon
= epsilon
219 def extendminmax(self
, min, max, preexp
, extendmin
, extendmax
):
222 for i
in range(len(preexp
.pres
)):
223 imin
= int(math
.floor(math
.log(min / float(preexp
.pres
[i
])) /
224 math
.log(preexp
.exp
) + self
.epsilon
)) + 1
225 imax
= int(math
.ceil(math
.log(max / float(preexp
.pres
[i
])) /
226 math
.log(preexp
.exp
) - self
.epsilon
)) - 1
227 if minpower
is None or imin
< minpower
:
228 minpower
, minindex
= imin
, i
229 if maxpower
is None or imax
>= maxpower
:
230 maxpower
, maxindex
= imax
, i
232 minrational
= preexp
.pres
[minindex
- 1]
234 minrational
= preexp
.pres
[-1]
236 if maxindex
!= len(preexp
.pres
) - 1:
237 maxrational
= preexp
.pres
[maxindex
+ 1]
239 maxrational
= preexp
.pres
[0]
242 min = float(minrational
) * float(preexp
.exp
) ** minpower
244 max = float(maxrational
) * float(preexp
.exp
) ** maxpower
247 def getticks(self
, min, max, preexp
, ticklevel
=None, labellevel
=None):
251 for f
in preexp
.pres
:
253 imin
= int(math
.ceil(math
.log(min / float(f
)) /
254 math
.log(preexp
.exp
) - 0.5 * self
.epsilon
))
255 imax
= int(math
.floor(math
.log(max / float(f
)) /
256 math
.log(preexp
.exp
) + 0.5 * self
.epsilon
))
257 for i
in range(imin
, imax
+ 1):
258 pos
= f
* tick
.rational((preexp
.exp
, 1), power
=i
)
259 thisticks
.append(tick
.tick((pos
.num
, pos
.denom
), ticklevel
= ticklevel
, labellevel
= labellevel
))
260 ticks
= tick
.mergeticklists(ticks
, thisticks
)
266 class autologarithmic(logarithmic
):
267 """partitioner to create several logarithmic partitions"""
269 defaultvariants
= [([logarithmic
.pre1exp
, # ticks
270 logarithmic
.pre1to9exp
], # subticks
271 [logarithmic
.pre1exp
, # labels
272 logarithmic
.pre125exp
]), # sublevels
274 ([logarithmic
.pre1exp
, # ticks
275 logarithmic
.pre1to9exp
], # subticks
276 None)] # labels like ticks
278 def __init__(self
, variants
=defaultvariants
, extendtick
=0, extendlabel
=None, autoexponent
=10, epsilon
=1e-10):
279 self
.variants
= variants
280 self
.extendtick
= extendtick
281 self
.extendlabel
= extendlabel
282 self
.autoexponent
= autoexponent
283 self
.epsilon
= epsilon
285 def partfunctions(self
, min, max, extendmin
, extendmax
):
286 return [lambda d
=_partdata(min=min, max=max, extendmin
=extendmin
, extendmax
=extendmax
,
287 variantsindex
=len(self
.variants
)):
288 self
.variantspartfunction(d
),
289 lambda d
=_partdata(min=min, max=max, extendmin
=extendmin
, extendmax
=extendmax
,
290 exponent
=self
.autoexponent
):
291 self
.autopartfunction(d
)]
293 def variantspartfunction(self
, data
):
294 data
.variantsindex
-= 1
295 if 0 <= data
.variantsindex
:
296 logarithmicparter
= logarithmic(tickpreexps
=self
.variants
[data
.variantsindex
][0], labelpreexps
=self
.variants
[data
.variantsindex
][1],
297 extendtick
=self
.extendtick
, extendlabel
=self
.extendlabel
, epsilon
=self
.epsilon
)
298 return logarithmicparter
.partfunctions(min=data
.min, max=data
.max, extendmin
=data
.extendmin
, extendmax
=data
.extendmax
)[0]()
301 def autopartfunction(self
, data
):
302 data
.exponent
*= self
.autoexponent
303 logarithmicparter
= logarithmic(tickpreexps
=[preexp([tick
.rational((1, 1))], data
.exponent
), logarithmic
.pre1exp
],
304 extendtick
=self
.extendtick
, extendlabel
=self
.extendlabel
, epsilon
=self
.epsilon
)
305 return logarithmicparter
.partfunctions(min=data
.min, max=data
.max, extendmin
=data
.extendmin
, extendmax
=data
.extendmax
)[0]()
307 autolog
= autologarithmic