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