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