pub struct Weights {
simplicity: f64,
coverage: f64,
density: f64,
legibility: f64,
}
impl Default for Weights {
fn default() -> Self {
Self {
simplicity: 0.25,
coverage: 0.2,
density: 0.5,
legibility: 0.05,
}
}
}
impl Weights {
pub fn score(&self, s: f64, c: f64, d: f64, l: f64) -> f64 {
self.simplicity * s + self.coverage * c + self.density * d + self.legibility * l
}
}
const DEFAULT_Q: &[f64] = &[1., 5., 2., 2.5, 4., 3.];
pub fn talbot(range: std::ops::Range<f64>, num_labels: usize) -> Vec<f64> {
talbot_custom(range, num_labels, DEFAULT_Q, false, Weights::default())
}
pub fn talbot_custom(
range: std::ops::Range<f64>,
num_labels: usize,
nice_numbers: &[f64],
loose: bool,
weights: Weights,
) -> Vec<f64> {
let mut best_lmin = 0.0;
let mut best_lmax = 0.0;
let mut best_lstep = 0.0;
let mut best_score = -2.0;
'scope: for j in 1.. {
for q in nice_numbers {
let sm = simplicity_max(*q, nice_numbers, j);
if weights.score(sm, 1., 1., 1.) < best_score {
break 'scope;
}
for k in 2.. {
let dm = density_max(k, num_labels);
if weights.score(sm, 1., dm, 1.) < best_score {
break;
}
let delta = (range.end - range.start) / (k + 1) as f64 / j as f64 / q;
for z in delta.log10().ceil() as i32.. {
let step = j as f64 * q * f64::powi(10.0, z);
let cm = coverage_max(&range, step * (k - 1) as f64);
if weights.score(sm, cm, dm, 1.) < best_score {
break;
}
let min_start =
(range.end / step).floor() as i64 * j as i64 - (k - 1) as i64 * j as i64;
let max_start = (range.start / step).ceil() as i64 * j as i64;
if min_start > max_start {
continue;
}
for start in min_start..=max_start {
let lmin = start as f64 * (step / j as f64);
let lmax = lmin + step * (k - 1) as f64;
let lstep = step;
let s = simplicity(*q, nice_numbers, j, lmin, lmax, lstep);
let c = coverage(&range, lmin, lmax);
let g = density(k, num_labels, &range, lmin, lmax);
let l = legibility(lmin, lmax, lstep);
let score = weights.score(s, c, g, l);
if j == 1 && k == 6 && z == 0 && start == 9 {
println!(
"simplicity = {}",
simplicity(1., nice_numbers, 1, 9., 14., 1.)
);
println!("q={} lmin={} lmax={} lstep={}", q, lmin, lmax, lstep);
println!("s={} c={} g={} l={}", s, c, g, l);
}
if score > best_score
&& (!loose || (lmin <= range.start && lmax >= range.end))
{
best_score = score;
best_lmin = lmin;
best_lmax = lmax;
best_lstep = lstep;
}
}
}
}
}
}
linspace(best_lmin..best_lmax, best_lstep)
}
use super::linspace;
fn simplicity(q: f64, nice_numbers: &[f64], j: usize, lmin: f64, lmax: f64, lstep: f64) -> f64 {
let i = nice_numbers.iter().position(|&x| x == q).unwrap();
let v = if (lmin.rem_euclid(lstep) < f64::EPSILON
|| lstep - lmin.rem_euclid(lstep) < f64::EPSILON)
&& lmin <= 0.0
&& lmax >= 0.0
{
1.0
} else {
0.0
};
1.0 - i as f64 / (nice_numbers.len() - 1) as f64 - j as f64 + v
}
fn simplicity_max(q: f64, nice_numbers: &[f64], j: usize) -> f64 {
let i = nice_numbers.iter().position(|&x| x == q).unwrap();
1.0 - i as f64 / (nice_numbers.len() - 1) as f64 - j as f64 + 1.0
}
fn coverage(range: &std::ops::Range<f64>, lmin: f64, lmax: f64) -> f64 {
let d = range.end - range.start;
1.0 - 0.5 * ((range.end - lmax).powi(2) + (range.start - lmin).powi(2)) / ((0.1 * d).powi(2))
}
fn coverage_max(range: &std::ops::Range<f64>, span: f64) -> f64 {
let d = range.end - range.start;
if span > d {
let half = (span - d) / 2.0;
1.0 - 0.5 * (half * half * 2.0) / ((0.1 * d).powi(2))
} else {
1.0
}
}
fn density(k: usize, num_labels: usize, range: &std::ops::Range<f64>, lmin: f64, lmax: f64) -> f64 {
let r = (k - 1) as f64 / (lmax - lmin);
let rt = (num_labels - 1) as f64 / (range.end.max(lmax) - range.start.min(lmin));
2.0 - (r / rt).max(rt / r)
}
fn density_max(k: usize, num_labels: usize) -> f64 {
if k >= num_labels {
2.0 - (k - 1) as f64 / (num_labels - 1) as f64
} else {
1.0
}
}
fn legibility(_lmin: f64, _lmax: f64, _lstep: f64) -> f64 {
1.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(simplicity(1., DEFAULT_Q, 1, 9., 14., 1.), 0.0);
talbot(8.1..14.1, 4);
}
}