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  }