1# -*- encoding: utf-8 -*- 2# 3# 4# Copyright (C) 2002-2004 Jörg Lehmann <joerg@pyx-project.org> 5# Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net> 6# Copyright (C) 2002-2012 André Wobst <wobsta@pyx-project.org> 7# 8# This file is part of PyX (https://pyx-project.org/). 9# 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. 14# 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. 19# 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 23 24 25from pyx import unit, box 26from pyx.graph.axis import tick 27 28 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) 37 38 39class 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""" 47 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 61 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) 77 78 79class 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.)""" 91 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 98 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)) 115 116 117class 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)""" 134 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 156 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 if not maxticklevel and not maxlabellevel: 170 return None 171 numticks = [0]*maxticklevel 172 numlabels = [0]*maxlabellevel 173 for t in ticks: 174 if t.ticklevel is not None: 175 for level in range(t.ticklevel, maxticklevel): 176 numticks[level] += 1 177 if t.labellevel is not None: 178 for level in range(t.labellevel, maxlabellevel): 179 numlabels[level] += 1 180 rate = 0 181 weight = 0 182 for numtick, rater in zip(numticks, self.ticks): 183 rate += rater.rate(numtick, density) 184 weight += rater.weight 185 for numlabel, rater in zip(numlabels, self.labels): 186 rate += rater.rate(numlabel, density) 187 weight += rater.weight 188 return rate/weight 189 190 def raterange(self, tickrange, datarange): 191 """rate the range covered by the ticks compared to the range 192 of the data 193 - tickrange and datarange are the ranges covered by the ticks 194 and the data in graph coordinates 195 - usually, the datarange is 1 (ticks are calculated for a 196 given datarange) 197 - the ticks might cover less or more than the data range (for 198 the standard automatic axis partition schemes an extention 199 of the axis range is normal and should get some penalty)""" 200 return self.range.rate(tickrange, datarange) 201 202 def ratelayout(self, axiscanvas, density): 203 """rate distances of the labels in an axis canvas 204 - the distances should be collected as box distances of 205 subsequent labels 206 - the axiscanvas provides a labels attribute for easy 207 access to the labels whose distances have to be taken 208 into account 209 - the density is used within the distancerate instance""" 210 if axiscanvas.labels is None: # to disable any layout rating 211 return 0 212 if len(axiscanvas.labels) > 1: 213 try: 214 distances = [axiscanvas.labels[i].boxdistance_pt(axiscanvas.labels[i+1]) 215 for i in range(len(axiscanvas.labels) - 1)] 216 except box.BoxCrossError: 217 return None 218 return self.distance.rate(distances, density) 219 else: 220 return None 221 222 223class linear(rater): 224 """a rater with predefined constructor arguments suitable for a linear axis""" 225 226 def __init__(self, ticks=[cube(4), cube(10, weight=0.5)], 227 labels=[cube(4)], 228 range=cube(1, weight=2), 229 distance=distance(1*unit.v_cm)): 230 rater.__init__(self, ticks, labels, range, distance) 231 232lin = linear 233 234 235class logarithmic(rater): 236 """a rater with predefined constructor arguments suitable for a logarithmic axis""" 237 238 def __init__(self, ticks=[cube(5, right=20), cube(20, right=100, weight=0.5)], 239 labels=[cube(5, right=20), cube(5, right=20, weight=0.5)], 240 range=cube(1, weight=2), 241 distance=distance(1*unit.v_cm)): 242 rater.__init__(self, ticks, labels, range, distance) 243 244log = logarithmic 245