275 lines
7.6 KiB
Go
275 lines
7.6 KiB
Go
// 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
|
|
}
|