go-hep.org/x/hep@v0.38.1/hplot/internal/talbot/labelling.go (about) 1 // Copyright ©2020 The go-hep Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Copyright ©2017 The Gonum Authors. All rights reserved. 6 // Use of this source code is governed by a BSD-style 7 // license that can be found in the LICENSE file. 8 9 // This is an implementation of the Talbot, Lin and Hanrahan algorithm 10 // described in doi:10.1109/TVCG.2010.130 with reference to the R 11 // implementation in the labeling package, ©2014 Justin Talbot (Licensed 12 // MIT+file LICENSE|Unlimited). 13 14 package talbot 15 16 import ( 17 "math" 18 ) 19 20 const ( 21 // dlamchE is the machine epsilon. For IEEE this is 2^{-53}. 22 dlamchE = 1.0 / (1 << 53) 23 24 // dlamchB is the radix of the machine (the base of the number system). 25 dlamchB = 2 26 27 // dlamchP is base * eps. 28 dlamchP = dlamchB * dlamchE 29 ) 30 31 const ( 32 // free indicates no restriction on label containment. 33 free = iota 34 // containData specifies that all the data range lies 35 // within the interval [label_min, label_max]. 36 containData 37 // withinData specifies that all labels lie within the 38 // interval [dMin, dMax]. 39 withinData 40 ) 41 42 // talbotLinHanrahan returns an optimal set of approximately want label values 43 // for the data range [dMin, dMax], and the step and magnitude of the step between values. 44 // containment is specifies are guarantees for label and data range containment, valid 45 // values are free, containData and withinData. 46 // The optional parameters Q, nice numbers, and w, weights, allow tuning of the 47 // algorithm but by default (when nil) are set to the parameters described in the 48 // paper. 49 // The legibility function allows tuning of the legibility assessment for labels. 50 // By default, when nil, legbility will set the legibility score for each candidate 51 // labelling scheme to 1. 52 // See the paper for an explanation of the function of Q, w and legibility. 53 func talbotLinHanrahan(dMin, dMax float64, want int, containment int, Q []float64, w *weights, legibility func(lMin, lMax, lStep float64) float64) (values []float64, step, q float64, magnitude int) { 54 const eps = dlamchP * 100 55 56 if dMin > dMax { 57 panic("labelling: invalid data range: min greater than max") 58 } 59 60 if Q == nil { 61 Q = []float64{1, 5, 2, 2.5, 4, 3} 62 } 63 if w == nil { 64 w = &weights{ 65 simplicity: 0.25, 66 coverage: 0.2, 67 density: 0.5, 68 legibility: 0.05, 69 } 70 } 71 if legibility == nil { 72 legibility = unitLegibility 73 } 74 75 if r := dMax - dMin; r < eps { 76 l := make([]float64, want) 77 step := r / float64(want-1) 78 for i := range l { 79 l[i] = dMin + float64(i)*step 80 } 81 magnitude = minAbsMag(dMin, dMax) 82 return l, step, 0, magnitude 83 } 84 85 type selection struct { 86 // n is the number of labels selected. 87 n int 88 // lMin and lMax are the selected min 89 // and max label values. lq is the q 90 // chosen. 91 lMin, lMax, lStep, lq float64 92 // score is the score for the selection. 93 score float64 94 // magnitude is the magnitude of the 95 // label step distance. 96 magnitude int 97 } 98 best := selection{score: -2} 99 100 outer: 101 for skip := 1; ; skip++ { 102 for _, q := range Q { 103 sm := maxSimplicity(q, Q, skip) 104 if w.score(sm, 1, 1, 1) < best.score { 105 break outer 106 } 107 108 for have := 2; ; have++ { 109 dm := maxDensity(have, want) 110 if w.score(sm, 1, dm, 1) < best.score { 111 break 112 } 113 114 delta := (dMax - dMin) / float64(have+1) / float64(skip) / q 115 116 const maxExp = 309 117 for mag := int(math.Ceil(math.Log10(delta))); mag < maxExp; mag++ { 118 step := float64(skip) * q * math.Pow10(mag) 119 120 cm := maxCoverage(dMin, dMax, step*float64(have-1)) 121 if w.score(sm, cm, dm, 1) < best.score { 122 break 123 } 124 125 fracStep := step / float64(skip) 126 kStep := step * float64(have-1) 127 128 minStart := (math.Floor(dMax/step) - float64(have-1)) * float64(skip) 129 maxStart := math.Ceil(dMax/step) * float64(skip) 130 for start := minStart; start <= maxStart && start != start-1; start++ { 131 lMin := start * fracStep 132 lMax := lMin + kStep 133 134 switch containment { 135 case containData: 136 if dMin < lMin || lMax < dMax { 137 continue 138 } 139 case withinData: 140 if lMin < dMin || dMax < lMax { 141 continue 142 } 143 case free: 144 // Free choice. 145 } 146 147 score := w.score( 148 simplicity(q, Q, skip, lMin, lMax, step), 149 coverage(dMin, dMax, lMin, lMax), 150 density(have, want, dMin, dMax, lMin, lMax), 151 legibility(lMin, lMax, step), 152 ) 153 if score > best.score { 154 best = selection{ 155 n: have, 156 lMin: lMin, 157 lMax: lMax, 158 lStep: float64(skip) * q, 159 lq: q, 160 score: score, 161 magnitude: mag, 162 } 163 } 164 } 165 } 166 } 167 } 168 } 169 170 if best.score == -2 { 171 l := make([]float64, want) 172 step := (dMax - dMin) / float64(want-1) 173 for i := range l { 174 l[i] = dMin + float64(i)*step 175 } 176 magnitude = minAbsMag(dMin, dMax) 177 return l, step, 0, magnitude 178 } 179 180 l := make([]float64, best.n) 181 step = best.lStep * math.Pow10(best.magnitude) 182 for i := range l { 183 l[i] = best.lMin + float64(i)*step 184 } 185 return l, best.lStep, best.lq, best.magnitude 186 } 187 188 // minAbsMag returns the minumum magnitude of the absolute values of a and b. 189 func minAbsMag(a, b float64) int { 190 return int(math.Min(math.Floor(math.Log10(math.Abs(a))), (math.Floor(math.Log10(math.Abs(b)))))) 191 } 192 193 // simplicity returns the simplicity score for how will the curent q, lMin, lMax, 194 // lStep and skip match the given nice numbers, Q. 195 func simplicity(q float64, Q []float64, skip int, lMin, lMax, lStep float64) float64 { 196 const eps = dlamchP * 100 197 198 for i, v := range Q { 199 if v == q { 200 m := math.Mod(lMin, lStep) 201 v = 0 202 if (m < eps || lStep-m < eps) && lMin <= 0 && 0 <= lMax { 203 v = 1 204 } 205 return 1 - float64(i)/(float64(len(Q))-1) - float64(skip) + v 206 } 207 } 208 panic("labelling: invalid q for Q") 209 } 210 211 // maxSimplicity returns the maximum simplicity for q, Q and skip. 212 func maxSimplicity(q float64, Q []float64, skip int) float64 { 213 for i, v := range Q { 214 if v == q { 215 return 1 - float64(i)/(float64(len(Q))-1) - float64(skip) + 1 216 } 217 } 218 panic("labelling: invalid q for Q") 219 } 220 221 // coverage returns the coverage score for based on the average 222 // squared distance between the extreme labels, lMin and lMax, and 223 // the extreme data points, dMin and dMax. 224 func coverage(dMin, dMax, lMin, lMax float64) float64 { 225 r := 0.1 * (dMax - dMin) 226 max := dMax - lMax 227 min := dMin - lMin 228 return 1 - 0.5*(max*max+min*min)/(r*r) 229 } 230 231 // maxCoverage returns the maximum coverage achievable for the data 232 // range. 233 func maxCoverage(dMin, dMax, span float64) float64 { 234 r := dMax - dMin 235 if span <= r { 236 return 1 237 } 238 h := 0.5 * (span - r) 239 r *= 0.1 240 return 1 - (h*h)/(r*r) 241 } 242 243 // density returns the density score which measures the goodness of 244 // the labelling density compared to the user defined target 245 // based on the want parameter given to talbotLinHanrahan. 246 func density(have, want int, dMin, dMax, lMin, lMax float64) float64 { 247 rho := float64(have-1) / (lMax - lMin) 248 rhot := float64(want-1) / (math.Max(lMax, dMax) - math.Min(dMin, lMin)) 249 if d := rho / rhot; d >= 1 { 250 return 2 - d 251 } 252 return 2 - rhot/rho 253 } 254 255 // maxDensity returns the maximum density score achievable for have and want. 256 func maxDensity(have, want int) float64 { 257 if have < want { 258 return 1 259 } 260 return 2 - float64(have-1)/float64(want-1) 261 } 262 263 // unitLegibility returns a default legibility score ignoring label 264 // spacing. 265 func unitLegibility(_, _, _ float64) float64 { 266 return 1 267 } 268 269 // weights is a helper type to calcuate the labelling scheme's total score. 270 type weights struct { 271 simplicity, coverage, density, legibility float64 272 } 273 274 // score returns the score for a labelling scheme with simplicity, s, 275 // coverage, c, density, d and legibility l. 276 func (w *weights) score(s, c, d, l float64) float64 { 277 return w.simplicity*s + w.coverage*c + w.density*d + w.legibility*l 278 }