ezel 0.0.1

A good plotting library
Documentation
// official homepage: http://justintalbot.com/publication/extension-of-wilkinson/
/// official R code from the author: https://rdrr.io/cran/labeling/src/R/labeling.R
/// official repo: https://github.com/jtalbot/Labeling
/// c port by lokimx88: https://github.com/lokimx88/WilkinsonExtended

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.. {
                    // println!("j={} k={} z={} best={}", j, k, z, best_score);
                    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)
}

// assert_eq!(simplicity(1., DEFAULT_Q, 1, 9., 14., 1.), 0.0);
//                       q              j  lmin lmax lstep

use super::linspace;

/// i = 0;
/// 1.0 - 0 - 1 + 0
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);
        // assert_eq!(talbot(8.1..14.1, 4), vec![5.0, 10.0, 15.0]);
    }
}