d3-array icon indicating copy to clipboard operation
d3-array copied to clipboard

Missing bin due to floating-point error

Open Fil opened this issue 8 months ago • 3 comments

const values = [0.9299999999999999, 1.07];
d3.bin().thresholds(500)(values).filter((d) => d.length);

this returns: [[1.07, x0: 1.07, x1: 1.0702]]; the first value has disappeared.

Fil avatar Mar 16 '25 10:03 Fil

The problem occurs in the nice subroutine where the domain is erroneously niced to [0.93, 1.07]; the assumption is that the nice domain must subsume the original domain, but that’s not true with this input. That’s because:

Math.ceil(0.9299999999999999 * -5000) / -5000 // 0.93

Also:

d3.nice(0.9299999999999999, 1.07, 5000) // [0.93, 1.07]

The niced domain should be [0.9298, 1.07] instead. We probably need a threshold test instead of assuming that ceil and floor produce the desired result.

mbostock avatar Mar 16 '25 15:03 mbostock

(That said, I’m curious where the 0.9299999999999999 is coming from? Because if we control that code, we should try to get it to generate 0.93 instead which would avoid this problem.)

mbostock avatar Mar 16 '25 15:03 mbostock

This number is initially caused by a floating point error in this code I'm playing with:

  const data = [0, 2, ...Array.from({ length: 18 }, () => 1)]; // Array(20)
  const lo = d3.quantile(data, 0.05);
  const hi = d3.quantile(data, 0.95);
  const delta = (hi - lo) * 0.2;

  const bins = d3.bin()
    .thresholds(500)
    .value((d) => clamp(d, lo - delta, hi + delta))(data);

  return bins.filter((d) => d.length); // [Array(18), Array(1)] 🌶 there should be 20 total

  function clamp(x, lo, hi) {
    return x < lo ? lo : x > hi ? hi : x;
  }

(I've "fixed" my code by using 1/7 instead of 0.2…)

Fil avatar Mar 16 '25 17:03 Fil