graph manual finished for 0.6 and small cleanups
[PyX/mjg.git] / pyx / graph / axis / rater.py
blobc3cf2349c2ae4e6da168b2dbfde004429d051e86
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 from pyx import unit, box
27 from pyx.graph.axis import tick
30 # rater
31 # conseptional remarks:
32 # - raters are used to calculate a rating for a realization of something
33 # - a rating means a positive floating point value
34 # - ratings are used to order those realizations by their suitability
35 # (small ratings are better)
36 # - a rating of None means not suitable at all (those realizations should be
37 # thrown out)
40 class cube:
41 """a value rater
42 - a cube rater has an optimal value, where the rate becomes zero
43 - for a left (below the optimum) and a right value (above the optimum),
44 the rating is value is set to 1 (modified by an overall weight factor
45 for the rating)
46 - the analytic form of the rating is cubic for both, the left and
47 the right side of the rater, independently"""
49 # __implements__ = sole implementation
51 def __init__(self, opt, left=None, right=None, weight=1):
52 """initializes the rater
53 - by default, left is set to zero, right is set to 3*opt
54 - left should be smaller than opt, right should be bigger than opt
55 - weight should be positive and is a factor multiplicated to the rates"""
56 if left is None:
57 left = 0
58 if right is None:
59 right = 3*opt
60 self.opt = opt
61 self.left = left
62 self.right = right
63 self.weight = weight
65 def rate(self, value, density):
66 """returns a rating for a value
67 - the density lineary rescales the rater (the optimum etc.),
68 e.g. a value bigger than one increases the optimum (when it is
69 positive) and a value lower than one decreases the optimum (when
70 it is positive); the density itself should be positive"""
71 opt = self.opt * density
72 if value < opt:
73 other = self.left * density
74 elif value > opt:
75 other = self.right * density
76 else:
77 return 0
78 factor = (value - opt) / float(other - opt)
79 return self.weight * (factor ** 3)
82 class distance:
83 # TODO: update docstring
84 """a distance rater (rates a list of distances)
85 - the distance rater rates a list of distances by rating each independently
86 and returning the average rate
87 - there is an optimal value, where the rate becomes zero
88 - the analytic form is linary for values above the optimal value
89 (twice the optimal value has the rating one, three times the optimal
90 value has the rating two, etc.)
91 - the analytic form is reciprocal subtracting one for values below the
92 optimal value (halve the optimal value has the rating one, one third of
93 the optimal value has the rating two, etc.)"""
95 # __implements__ = sole implementation
97 def __init__(self, opt, weight=0.1):
98 """inititializes the rater
99 - opt is the optimal length (a visual PyX length)
100 - weight should be positive and is a factor multiplicated to the rates"""
101 self.opt_str = opt
102 self.weight = weight
104 def rate(self, distances, density):
105 """rate distances
106 - the distances are a list of positive floats in PostScript points
107 - the density lineary rescales the rater (the optimum etc.),
108 e.g. a value bigger than one increases the optimum (when it is
109 positive) and a value lower than one decreases the optimum (when
110 it is positive); the density itself should be positive"""
111 if len(distances):
112 opt = unit.topt(unit.length(self.opt_str, default_type="v")) / density
113 rate = 0
114 for distance in distances:
115 if distance < opt:
116 rate += self.weight * (opt / distance - 1)
117 else:
118 rate += self.weight * (distance / opt - 1)
119 return rate / float(len(distances))
122 class rater:
123 """a rater for ticks
124 - the rating of axes is splited into two separate parts:
125 - rating of the ticks in terms of the number of ticks, subticks,
126 labels, etc.
127 - rating of the label distances
128 - in the end, a rate for ticks is the sum of these rates
129 - it is useful to first just rate the number of ticks etc.
130 and selecting those partitions, where this fits well -> as soon
131 as an complete rate (the sum of both parts from the list above)
132 of a first ticks is below a rate of just the number of ticks,
133 subticks labels etc. of other ticks, those other ticks will never
134 be better than the first one -> we gain speed by minimizing the
135 number of ticks, where label distances have to be taken into account)
136 - both parts of the rating are shifted into instances of raters
137 defined above --- right now, there is not yet a strict interface
138 for this delegation (should be done as soon as it is needed)"""
140 # __implements__ = sole implementation
142 def __init__(self, ticks, labels, range, distance):
143 """initializes the axis rater
144 - ticks and labels are lists of instances of a value rater
145 - the first entry in ticks rate the number of ticks, the
146 second the number of subticks, etc.; when there are no
147 ticks of a level or there is not rater for a level, the
148 level is just ignored
149 - labels is analogous, but for labels
150 - within the rating, all ticks with a higher level are
151 considered as ticks for a given level
152 - range is a value rater instance, which rates the covering
153 of an axis range by the ticks (as a relative value of the
154 tick range vs. the axis range), ticks might cover less or
155 more than the axis range (for the standard automatic axis
156 partition schemes an extention of the axis range is normal
157 and should get some penalty)
158 - distance is an distance rater instance"""
159 self.ticks = ticks
160 self.labels = labels
161 self.range = range
162 self.distance = distance
164 def rateticks(self, axis, ticks, density):
165 """rates ticks by the number of ticks, subticks, labels etc.
166 - takes into account the number of ticks, subticks, labels
167 etc. and the coverage of the axis range by the ticks
168 - when there are no ticks of a level or there was not rater
169 given in the constructor for a level, the level is just
170 ignored
171 - the method returns the sum of the rating results divided
172 by the sum of the weights of the raters
173 - within the rating, all ticks with a higher level are
174 considered as ticks for a given level"""
175 maxticklevel, maxlabellevel = tick.maxlevels(ticks)
176 numticks = [0]*maxticklevel
177 numlabels = [0]*maxlabellevel
178 for t in ticks:
179 if t.ticklevel is not None:
180 for level in range(t.ticklevel, maxticklevel):
181 numticks[level] += 1
182 if t.labellevel is not None:
183 for level in range(t.labellevel, maxlabellevel):
184 numlabels[level] += 1
185 rate = 0
186 weight = 0
187 for numtick, rater in zip(numticks, self.ticks):
188 rate += rater.rate(numtick, density)
189 weight += rater.weight
190 for numlabel, rater in zip(numlabels, self.labels):
191 rate += rater.rate(numlabel, density)
192 weight += rater.weight
193 return rate/weight
195 def raterange(self, tickrange, datarange):
196 """rate the range covered by the ticks compared to the range
197 of the data
198 - tickrange and datarange are the ranges covered by the ticks
199 and the data in graph coordinates
200 - usually, the datarange is 1 (ticks are calculated for a
201 given datarange)
202 - the ticks might cover less or more than the data range (for
203 the standard automatic axis partition schemes an extention
204 of the axis range is normal and should get some penalty)"""
205 return self.range.rate(tickrange, datarange)
207 def ratelayout(self, axiscanvas, density):
208 """rate distances of the labels in an axis canvas
209 - the distances should be collected as box distances of
210 subsequent labels
211 - the axiscanvas provides a labels attribute for easy
212 access to the labels whose distances have to be taken
213 into account
214 - the density is used within the distancerate instance"""
215 if len(axiscanvas.labels) > 1:
216 try:
217 distances = [axiscanvas.labels[i].boxdistance_pt(axiscanvas.labels[i+1])
218 for i in range(len(axiscanvas.labels) - 1)]
219 except box.BoxCrossError:
220 return None
221 return self.distance.rate(distances, density)
222 else:
223 return None
226 class linear(rater):
227 """a rater with predefined constructor arguments suitable for a linear axis"""
229 def __init__(self, ticks=[cube(4), cube(10, weight=0.5)],
230 labels=[cube(4)],
231 range=cube(1, weight=2),
232 distance=distance("1 cm")):
233 rater.__init__(self, ticks, labels, range, distance)
235 lin = linear
238 class logarithmic(rater):
239 """a rater with predefined constructor arguments suitable for a logarithmic axis"""
241 def __init__(self, ticks=[cube(5, right=20), cube(20, right=100, weight=0.5)],
242 labels=[cube(5, right=20), cube(5, right=20, weight=0.5)],
243 range=cube(1, weight=2),
244 distance=distance("1 cm")):
245 rater.__init__(self, ticks, labels, range, distance)
247 log = logarithmic