Functional Weave
Code in Python

charts.histogram-bins

Bin values into histogram bins on round edges, by Sturges' rule, Freedman-Diaconis, or a fixed width.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 17 tests, run in TypeScript, Python and Rust.

What it does

Sorts a sample into histogram bins with round edges: `histogramBins([1..10], "sturges", null)` gives five bins, 0-2, 2-4, ..., 8-10. Each bin counts values with `x0 <= v < x1`; the last bin also counts its right edge, so the maximum is never lost off the end.

## How many bins

For example

  • histogram_bins(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, sturges, —) → ×5 Sturges: ten values make five bins of width 2
  • histogram_bins(1, 2, 3, 4, 5, 6, 7, 8, 9, 100, sturges, —) → ×5 Sturges with an outlier: five wide bins
  • histogram_bins(1, 2, 3, 4, 5, 6, 7, 8, 9, 100, freedman-diaconis, —) → ×20 Freedman-Diaconis with the same outlier: narrow bins sized by the quartiles

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

def histogram_bins(values: Sequence[float], method: BinMethod, bin_width: Optional[float]) -> List[Bin]
valuesfloat[]the sample, in any order; empty gives no bins
methodBinMethodhow many bins: sturges, freedman-diaconis, or fixed-width
bin_widthfloat?the width for fixed-width, greater than 0; null for the other methods
returnsBin[]adjacent bins from the first edge at or below the minimum to the first at or above the maximum

The types it declares, generated into your project

BinMethod = Literal["sturges", "freedman-diaconis", "fixed-width"]

@dataclass(frozen=True)
class Bin:
    """One histogram bar: values from x0 up to but not including x1 (the last bin includes x1)."""

    x0: float
    x1: float
    count: int

Your code names it in one line, in the file that uses it

from fune.charts.histogram_bins import histogram_bins  # charts.histogram-bins@^1
impl/python.py · 82 lines · open · raw

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

import math
from typing import List, Optional, Sequence

from .charts_histogram_bins_types import Bin, BinMethod
from .charts_ticks_tick_step import tick_spec
from .math_pow import pow as pow_float  ← from math.pow ^1.0.0 · built alongside by fune
from .math_round_float import round_float  ← from math.round-float ^1.0.0 · built alongside by fune
from .stats_percentile import percentile  ← from stats.percentile ^1.0.0 · built alongside by fune

_MAX_BINS = 10000


def histogram_bins(values: Sequence[float], method: BinMethod, bin_width: Optional[float]) -> List[Bin]:
    """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."""
    if method == "fixed-width":
        if bin_width is None or isinstance(bin_width, bool) or not isinstance(bin_width, (int, float)) or not math.isfinite(bin_width) or bin_width <= 0:
            raise ValueError(f"fixed-width needs a binWidth greater than 0; got {bin_width}")
    elif method in ("sturges", "freedman-diaconis"):
        if bin_width is not None:
            raise ValueError(f"binWidth is only for fixed-width; pass null for {method}")
    else:
        raise ValueError(f'unknown bin method "{method}"')
    n = len(values)
    if n == 0:
        return []
    for v in values:
        if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v):
            raise ValueError(f"values must be finite numbers; got {v}")
    lo = float(min(values))
    hi = float(max(values))

    if method == "fixed-width":
        mul, div = float(bin_width), 1.0
    else:
        if lo == hi:
            return [Bin(x0=lo + 0.0, x1=hi + 0.0, count=n)]
        if method == "sturges":
            # ceil(log2 n) + 1, by doubling rather than a logarithm.
            bits, power = 0, 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.
            iqr = percentile(values, 75, "linear", 12) - percentile(values, 25, "linear", 12)
            width = 2 * iqr * pow_float(float(n), -1 / 3)
            bins = math.ceil((hi - lo) / width) if width > 0 else 1
            if bins > _MAX_BINS:
                raise ValueError(f"too many bins: more than {_MAX_BINS}")
            count = max(1, bins)
        mul, div = tick_spec(lo, hi, count)

    def edge(i: int) -> float:
        # One exact operation on a whole number, cleaned at 12 places.
        return round_float(i / div if div > 1 else i * mul, 12)

    first = math.floor(lo * div if div > 1 else lo / mul)
    while edge(first) > lo:
        first -= 1
    while edge(first + 1) <= lo:
        first += 1
    last = first + 1
    while edge(last) < hi:
        last += 1
        if last - first > _MAX_BINS:
            raise ValueError(f"too many bins: more than {_MAX_BINS}")

    edges = [edge(i) for i in range(first, last + 1)]
    counts = [0] * (len(edges) - 1)
    for v in values:
        a, b = 0, len(counts) - 1
        while a < b:
            mid = (a + b + 1) // 2
            if edges[mid] <= v:
                a = mid
            else:
                b = mid - 1
        counts[a] += 1
    return [Bin(x0=edges[i], x1=edges[i + 1], count=c) for i, c in enumerate(counts)]

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 4 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add charts.histogram-bins
Download for Python charts.histogram-bins-1.0.0-python.fune · 12,333 bytes sha256 32bad32a21074762d27474266304a90c2e3c30cfb44b143b19b81908c99ebbb5

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./charts.histogram-bins-1.0.0-python.fune, or fetch it from a terminal with fune pull charts.histogram-bins@1.0.0:python.

The whole function, every language, is one file too: charts.histogram-bins-1.0.0.fune, 20,490 bytes, sha256 670e89879fefdf330fd24fb0245816b60333ca6a97345040574f0e8cab8b1915. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

# fune: before charts.histogram-bins

after — your function gets the result and the arguments, and returns the final result.

# fune: after charts.histogram-bins

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

# fune: replace charts.ticks in charts.histogram-bins
# fune: replace math.pow in charts.histogram-bins
# fune: replace math.round-float in charts.histogram-bins
# fune: replace stats.percentile in charts.histogram-bins

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show charts.histogram-bins --steps.

# fune: step charts.histogram-bins after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
Sturges: ten values make five bins of width 2 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, sturges, — → ×5
Sturges with an outlier: five wide bins 1, 2, 3, 4, 5, 6, 7, 8, 9, 100, sturges, — → ×5
Freedman-Diaconis with the same outlier: narrow bins sized by the quartiles 1, 2, 3, 4, 5, 6, 7, 8, 9, 100, freedman-diaconis, — → ×20
Freedman-Diaconis with no spread in the quartiles falls back to one bin 5, 5, 5, 5, 9, freedman-diaconis, — → ×1
unsorted fractions on fifths 0.12, 0.47, 0.33, 0.91, sturges, — → ×5
fixed width 0.1: 0.3 is on the edge 0.3, not below 0.30000000000000004 0.05, 0.25, 0.3, 0.31, fixed-width, 0.1 → ×4
fixed width: the maximum on an edge goes in the last bin 0, 5, 10, fixed-width, 5 → ×2
fixed width across zero -7, -1, 3, fixed-width, 5 → ×3
fixed width with one value 7, fixed-width, 5 → ×1
one value with Sturges is a bin of no width 4.5, sturges, — → ×1
Show the other 7 tests
CaseArgumentsExpected
equal values with Sturges 3, 3, 3, sturges, — → ×1
no values, no bins , sturges, — →
fixed-width without a width is an error 1, 2, fixed-width, — → error: fixed-width needs a binWidth greater than 0
a zero width is an error 1, 2, fixed-width, 0 → error: fixed-width needs a binWidth greater than 0
a width with Sturges is an error 1, 2, sturges, 2 → error: binWidth is only for fixed-width
an unknown method is an error 1, 2, scott, — → error: unknown bin method "scott"
more than 10,000 bins is an error 0, 100, fixed-width, 0.001 → error: too many bins: more than 10000

More from the author

- **`sturges`**: ceil(log2 n) + 1 bins (Sturges 1926), d3.bin's default. It assumes a roughly normal sample and gives too few bins for large or skewed ones. log2 is found by doubling, not with a logarithm. - **`freedman-diaconis`**: bin width 2 x IQR x n^(-1/3) (Freedman and Diaconis 1981), which follows the middle half of the data and so is not stretched by an outlier. The quartiles are `stats.percentile` with the `linear` method (R-7, as d3's quantile), and n^(-1/3) is `math.pow`. When the interquartile range is zero it falls back to one bin, as d3 does. - **`fixed-width`**: the width you pass, with edges on its multiples (a width of 5 puts edges on ..., -5, 0, 5, 10, ...). `binWidth` must be null for the other two methods: an argument that would be silently ignored is an error.

For the first two, the count is turned into a round width with `charts.ticks` (1, 2 or 5 x 10^k for about that many bins), as d3.bin does with its nice thresholds, so edges fall on numbers a reader expects rather than on min + k x (max - min) / count.

## Edges

The first edge is the multiple of the width at or below the minimum and the last the first at or above the maximum. Each edge is one exact operation on a whole number (i x width, or i / divisor for a fractional round width) and is then cleaned at 12 decimal places with `math.round-float`, so a width of 0.1 has an edge at 0.3, not 0.30000000000000004 as `3 * 0.1` gives, and a value of exactly 0.3 lands in the bin starting there.

No values gives no bins; values that are all equal give one bin of no width under the first two methods. More than 10,000 bins is an error.

Sources: H. A. Sturges, "The Choice of a Class Interval", Journal of the American Statistical Association 21 (1926) 65-66; D. Freedman and P. Diaconis, "On the histogram as a density estimator: L2 theory", Zeitschrift für Wahrscheinlichkeitstheorie 57 (1981) 453-476; Mike Bostock, d3-array `bin.js` and `threshold/*.js`.

Files

PathBytes
README.md2,273
impl/python.py3,254
impl/rust.rs4,250
impl/typescript.ts3,595
vectors.json3,747