d3 / d3-scale

Encodings that map abstract data to visual representation.
https://d3js.org/d3-scale
ISC License
1.59k stars 286 forks source link

Not too nice ticks for big domains #264

Closed ZiedHf closed 2 years ago

ZiedHf commented 2 years ago

I am not sure if this is what ticks should return for these domains or not. Is there a limit for the max and min ? Or maybe I am missing something to return nicer ticks ? It looks like a floating points problem.

// Good
scaleLinear().domain([0, 1e-21]).ticks(10)
/* [ 0,  1e-22,  2e-22,  3e-22,  4e-22,  5e-22,  6e-22,  7e-22, 8e-22, 9e-22, 1e-21 ] */

// Weird
scaleLinear().domain([0, 1e-22]).ticks(10)
/* [ 0, 1.0000000000000001e-23, 2.0000000000000002e-23, 3e-23, 4.0000000000000004e-23, 5e-23, 6e-23,  7.000000000000001e-23, 8.000000000000001e-23, 9.000000000000001e-23, 1e-22 ] */

// Good
scaleLinear().domain([0, 1e+23]).ticks(10)
/* [ 0, 1e+22, 2e+22, 3e+22, 4e+22, 5e+22, 6e+22, 7e+22, 8e+22, 9e+22, 1e+23 ] */

// Weird
scaleLinear().domain([0, 1e+24]).ticks(10)
/* [ 0, 1e+23, 2e+23, 2.9999999999999997e+23, 4e+23, 5e+23, 5.9999999999999995e+23, 6.999999999999999e+23, 8e+23, 9e+23, 1e+24 ] */
mbostock commented 2 years ago

Not sure if we can do anything better here. Feel free to open a pull request if you have a suggestion for a change to the ticks algorithm.

ZiedHf commented 2 years ago

Yep, it's floating points issues.

// Problem
r0 + i * step;
0 + 3 * 2e+23; // This returns 5.9999999999999995e+23

It looks like using Big in the ticks method solve the issue. But maybe you don't prefer to use another dependencies to solve it ? I don't know.

Big(2e+23).mul(3).toString(); // 6e+23 Nice result !!

The ticks method

function ticks(start, stop, count) {
  var reverse,
      i = -1,
      n,
      ticks,
      step;

  stop = +stop, start = +start, count = +count;
  if (start === stop && count > 0) return [start];
  if (reverse = stop < start) n = start, start = stop, stop = n;
  if ((step = tickIncrement(start, stop, count)) === 0 || !isFinite(step)) return [];

  if (step > 0) {
    let r0 = Math.round(start / step), r1 = Math.round(stop / step);
    console.log(start, stop, step)
    console.log(r0, r1)
    if (r0 * step < start) ++r0;
    console.log(r0, r1)
    if (r1 * step > stop) --r1;
    console.log(r0, r1)
    ticks = new Array(n = r1 - r0 + 1);
    console.log(ticks, n)
    while (++i < n) ticks[i] = Number(Big(r0).add(i).mul(step).toString());
  } else {
    step = -step;
    let r0 = Math.round(start * step), r1 = Math.round(stop * step);
    if (r0 / step < start) ++r0;
    if (r1 / step > stop) --r1;
    ticks = new Array(n = r1 - r0 + 1);
    while (++i < n) ticks[i] = Number(Big(r0).add(i).div(step).toString());
  }

  if (reverse) ticks.reverse();

  return ticks;
}
mbostock commented 2 years ago

JavaScript has native support for big integers now, so we could use that, but I don’t think I’d want to use big integers in the common path (since it’s rare that the domain is so big or so small). And it would need to be a new major version since it would reduce compatibility of d3-array with older JavaScript environments. So I probably don’t think this is worth it unless there is more evidence of demand.