Functional Weave
Code in Rust

charts.histogram-bins@1.0.0

impl/typescript.ts

3,595 bytes · the TypeScript 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.

import { type Bin, type BinMethod } from "./charts_histogram_bins_types.ts";
import { percentile } from "./stats_percentile.ts";  ← from stats.percentile ^1.0.0 · built alongside by fune
import { tickSpec } from "./charts_ticks_tick_step.ts";
import { pow } from "./math_pow.ts";  ← from math.pow ^1.0.0 · built alongside by fune
import { roundFloat } from "./math_round_float.ts";  ← from math.round-float ^1.0.0 · built alongside by fune

const MAX_BINS = 10000;

/**
 * Histogram bins on round edges, as d3.bin lays them out: the method decides
 * roughly how many bins, a tick step of 1, 2 or 5 x 10^k turns that into a
 * width, and the edges are multiples of it from the first at or below the
 * minimum to the first at or above the maximum. Each bin holds x0 <= v < x1;
 * the last also holds its right edge, so the maximum is always counted.
 */
export function histogramBins(values: readonly number[], method: BinMethod, binWidth: number | null): readonly Bin[] {
  if (method === "fixed-width") {
    if (binWidth === null || typeof binWidth !== "number" || !Number.isFinite(binWidth) || binWidth <= 0) {
      throw new Error(`fixed-width needs a binWidth greater than 0; got ${binWidth}`);
    }
  } else if (method === "sturges" || method === "freedman-diaconis") {
    if (binWidth !== null) throw new Error(`binWidth is only for fixed-width; pass null for ${method}`);
  } else {
    throw new Error(`unknown bin method "${method}"`);
  }
  const n = values.length;
  if (n === 0) return [];
  let lo = values[0];
  let hi = values[0];
  for (const v of values) {
    if (typeof v !== "number" || !Number.isFinite(v)) throw new Error(`values must be finite numbers; got ${v}`);
    if (v < lo) lo = v;
    if (v > hi) hi = v;
  }

  let mul: number;
  let div: number;
  if (method === "fixed-width") {
    mul = binWidth as number;
    div = 1;
  } else {
    if (lo === hi) return [{ x0: lo + 0, x1: hi + 0, count: n }];
    let count: number;
    if (method === "sturges") {
      // ceil(log2 n) + 1, by doubling rather than a logarithm.
      let bits = 0;
      let power = 1;
      while (power < n) {
        power *= 2;
        bits += 1;
      }
      count = 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.
      const iqr = percentile(values, 75, "linear", 12) - percentile(values, 25, "linear", 12);
      const width = 2 * iqr * pow(n, -1 / 3);
      const bins = width > 0 ? Math.ceil((hi - lo) / width) : 1;
      if (bins > MAX_BINS) throw new Error(`too many bins: more than ${MAX_BINS}`);
      count = Math.max(1, bins);
    }
    [mul, div] = tickSpec(lo, hi, count);
  }

  // Edge i is one exact operation on a whole number, cleaned at 12 places so
  // a fixed width of 0.1 gives an edge of 0.3, not 0.30000000000000004.
  const edge = (i: number) => roundFloat(div > 1 ? i / div : i * mul, 12);
  let first = Math.floor(div > 1 ? lo * div : lo / mul);
  while (edge(first) > lo) first -= 1;
  while (edge(first + 1) <= lo) first += 1;
  let last = first + 1;
  while (edge(last) < hi) {
    last += 1;
    if (last - first > MAX_BINS) throw new Error(`too many bins: more than ${MAX_BINS}`);
  }

  const edges: number[] = [];
  for (let i = first; i <= last; i++) edges.push(edge(i));
  const counts = edges.slice(1).map(() => 0);
  for (const v of values) {
    // The last edge at or below v; the maximum falls in the last bin.
    let a = 0;
    let b = counts.length - 1;
    while (a < b) {
      const mid = Math.floor((a + b + 1) / 2);
      if (edges[mid] <= v) a = mid;
      else b = mid - 1;
    }
    counts[a] += 1;
  }
  return counts.map((count, i) => ({ x0: edges[i], x1: edges[i + 1], count }));
}