Functional Weave
Code in Rust

charts.scale@1.1.0

impl/rust/nice_domain.rs

2,738 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

/// Widen a domain to multiples of a round tick step (1, 2 or 5 times a power
/// of ten) chosen for about `count` ticks. Widening can change the best step,
/// so it repeats until the step settles, as d3's nice() does.
///
/// # Panics
/// Panics if the domain does not have exactly two values or `count` is less
/// than 1.
pub fn nice_domain(domain: &[f64], count: i64) -> NiceDomain {
    if domain.len() != 2 {
        panic!("domain must have exactly 2 values, [from, to]; got {}", domain.len());
    }
    if count < 1 {
        panic!("count must be a whole number of at least 1, got {}", count);
    }
    let reversed = domain[1] < domain[0];
    let (mut lo, mut hi) = if reversed { (domain[1], domain[0]) } else { (domain[0], domain[1]) };
    if lo == hi {
        return NiceDomain { domain: vec![domain[0], domain[1]], step: 0.0 };
    }

    let mut step = 0.0;
    for _ in 0..10 {
        let next = tick_step(lo, hi, count as f64);
        if next == step {
            break;
        }
        step = next;
        if step >= 1.0 {
            lo = (lo / step).floor() * step;
            hi = (hi / step).ceil() * step;
        } else {
            // A fractional step divides badly (0.3 / 0.1 is 2.9999999999999996),
            // so multiply by its whole-number inverse instead.
            let inverse = (1.0 / step + 0.5).floor();
            lo = (lo * inverse).floor() / inverse;
            hi = (hi * inverse).ceil() / inverse;
        }
    }
    let ends = if reversed { vec![round6(hi), round6(lo)] } else { vec![round6(lo), round6(hi)] };
    NiceDomain { domain: ends, step: round6(step) }
}

fn tick_step(lo: f64, hi: f64, count: f64) -> f64 {
    let raw = (hi - lo) / count;
    // Powers of ten by repeated multiplication rather than log10, whose last
    // digit is not guaranteed to agree between languages.
    let mut power = 1.0;
    while power * 10.0 <= raw {
        power *= 10.0;
    }
    while power > raw {
        power /= 10.0;
    }
    let error = raw / power;
    let factor = if error >= 50f64.sqrt() {
        10.0
    } else if error >= 10f64.sqrt() {
        5.0
    } else if error >= 2f64.sqrt() {
        2.0
    } else {
        1.0
    };
    factor * power
}

fn round6(x: f64) -> f64 {
    (x * 1e6 + 0.5).floor() / 1e6
}

pub fn nice_domain_to_value(nice: &NiceDomain) -> Value {
    Value::obj(vec![
        ("domain", Value::Arr(nice.domain.iter().map(|x| Value::Float(*x)).collect())),
        ("step", Value::Float(nice.step)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let domain: Vec<f64> = args[0].as_arr().iter().map(|v| v.as_f64()).collect();
    nice_domain_to_value(&nice_domain(&domain, args[1].as_i64()))
}