use super::funejson::Value; use super::charts_ticks_tick_step::tick_spec; use super::math_pow::pow; use super::math_round_float::round_float; use super::stats_percentile::percentile; 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) -> Vec { 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 = (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 = 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()) }