Functional Weave
Code in Python

charts.histogram-bins@1.0.0

impl/rust.rs

4,250 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
use super::charts_ticks_tick_step::tick_spec;
use super::math_pow::pow;  ← from math.pow ^1.0.0 · built alongside by fune
use super::math_round_float::round_float;  ← from math.round-float ^1.0.0 · built alongside by fune
use super::stats_percentile::percentile;  ← from stats.percentile ^1.0.0 · built alongside by fune

const MAX_BINS: i64 = 10000;

/// Histogram bins on round edges, as d3.bin lays them out. Each bin holds
/// x0 <= v < x1; the last also holds its right edge. See the README.
///
/// # Panics
/// Panics on an unknown method, a binWidth given for a method that does not
/// take one or missing for fixed-width, a non-finite value, or more than
/// 10,000 bins.
pub fn histogram_bins(values: &[f64], method: &str, bin_width: Option<f64>) -> Vec<Bin> {
    match method {
        "fixed-width" => match bin_width {
            Some(w) if w.is_finite() && w > 0.0 => {}
            Some(w) => panic!("fixed-width needs a binWidth greater than 0; got {}", w),
            None => panic!("fixed-width needs a binWidth greater than 0; got null"),
        },
        "sturges" | "freedman-diaconis" => {
            if bin_width.is_some() {
                panic!("binWidth is only for fixed-width; pass null for {}", method);
            }
        }
        other => panic!("unknown bin method \"{}\"", other),
    }
    let n = values.len();
    if n == 0 {
        return Vec::new();
    }
    let mut lo = values[0];
    let mut hi = values[0];
    for &v in values {
        if !v.is_finite() {
            panic!("values must be finite numbers; got {}", v);
        }
        if v < lo {
            lo = v;
        }
        if v > hi {
            hi = v;
        }
    }

    let (mul, div) = if method == "fixed-width" {
        (bin_width.unwrap(), 1.0)
    } else {
        if lo == hi {
            return vec![Bin { x0: lo + 0.0, x1: hi + 0.0, count: n as i64 }];
        }
        let count = if method == "sturges" {
            // ceil(log2 n) + 1, by doubling rather than a logarithm.
            let (mut bits, mut power) = (0i64, 1usize);
            while power < n {
                power *= 2;
                bits += 1;
            }
            bits + 1
        } else {
            // Freedman and Diaconis: bin width 2 IQR n^(-1/3); d3 falls back
            // to one bin when the interquartile range is zero.
            let iqr = percentile(values, 75.0, "linear", 12) - percentile(values, 25.0, "linear", 12);
            let width = 2.0 * iqr * pow(n as f64, -1.0 / 3.0);
            let bins = if width > 0.0 { ((hi - lo) / width).ceil() } else { 1.0 };
            if bins > MAX_BINS as f64 {
                panic!("too many bins: more than {}", MAX_BINS);
            }
            (bins as i64).max(1)
        };
        tick_spec(lo, hi, count)
    };

    // One exact operation on a whole number, cleaned at 12 places.
    let edge = |i: i64| round_float(if div > 1.0 { i as f64 / div } else { i as f64 * mul }, 12);
    let mut first = (if div > 1.0 { lo * div } else { lo / mul }).floor() as i64;
    while edge(first) > lo {
        first -= 1;
    }
    while edge(first + 1) <= lo {
        first += 1;
    }
    let mut last = first + 1;
    while edge(last) < hi {
        last += 1;
        if last - first > MAX_BINS {
            panic!("too many bins: more than {}", MAX_BINS);
        }
    }

    let edges: Vec<f64> = (first..=last).map(edge).collect();
    let mut counts = vec![0i64; edges.len() - 1];
    for &v in values {
        let (mut a, mut b) = (0usize, counts.len() - 1);
        while a < b {
            let mid = (a + b + 1) / 2;
            if edges[mid] <= v {
                a = mid;
            } else {
                b = mid - 1;
            }
        }
        counts[a] += 1;
    }
    counts
        .iter()
        .enumerate()
        .map(|(i, &count)| Bin { x0: edges[i], x1: edges[i + 1], count })
        .collect()
}

pub fn bin_to_value(bin: &Bin) -> Value {
    Value::obj(vec![("x0", Value::Float(bin.x0)), ("x1", Value::Float(bin.x1)), ("count", Value::Int(bin.count))])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let values: Vec<f64> = args[0].as_arr().iter().map(|v| v.as_f64()).collect();
    let width = if args[2].is_null() { None } else { Some(args[2].as_f64()) };
    Value::Arr(histogram_bins(&values, args[1].as_str(), width).iter().map(bin_to_value).collect())
}