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

optional log base for ticks functions

Open nickofthyme opened this issue 4 years ago • 6 comments
trafficstars

Adds optional base value to ticks, tickIncrement and tickStep functions. Using default base value of 10 results in no functional changes to existing code.

These changes enable rendering linear ticks for binary, natural and other log bases.

cc: @monfera

closes #232

nickofthyme avatar Sep 21 '21 17:09 nickofthyme

@mbostock any feedback on this?

nickofthyme avatar Oct 26 '21 21:10 nickofthyme

@Fil That's a really good point about the <base>^0. It appears this is a limitation with the implementation, I can't think of another way to get around this, anytime the power falls between 0 and 1 it just falls in that hole where the resulting step is 1 for any base (excluding 0).

y^x

image

I thought about forcing the power to be -1 for Math.E base such as...

function roundPower(power, base) {
  if (base !== Math.E) return Math.floor(power);
  return Math.floor(power) === 0 ? -1 : Math.floor(power);
}

export function tickIncrement(start, stop, count, base = 10) {
  var step = (stop - start) / Math.max(0, count),
      power = roundPower(Math.log(step) / Math.log(base) + Number.EPSILON, base),
      error = step / Math.pow(base, power);

  return power >= 0
      ? (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1) * Math.pow(base, power)
      : -Math.pow(base, -power) / (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1);
}

But then Math.E would be the only case this applies to which could be strange.

Another approach would be to allow 0.5 as a power. This would provide a much nicer tick values for cases in the 0 to 1 abyss. Something like...

function roundPower(power) {
  if (Math.floor(power) !== 0) return Math.floor(power);
  return power >= 0.5 ? 0.5 : 0;
}

With allowing 0.5 power you would get something like this...

// with power 0.5
ticks(0, 10,  3, 4) // [0, 4, 8]
// without power 0.5
ticks(0, 10,  3, 4) // [0, 5, 10]

This approach causes edge case issues with base 10

I'm inclined to just keep the value of power at 0. Another idea would be to round the power to some value, instead of flooring it, but that seems have a more broad impact and increased tick counts. Any thoughts?

nickofthyme avatar Oct 27 '21 17:10 nickofthyme

@Fil Is it possible for me to update the examples linked in the readme? Such as https://observablehq.com/@d3/d3-ticks

nickofthyme avatar Oct 28 '21 18:10 nickofthyme

@Fil any update on this?

nickofthyme avatar Nov 11 '21 22:11 nickofthyme

My concerns here:

  • Too many positional arguments (start, stop, count, base)
  • Other implicit dependencies on base 10 (e.g., 2 and 5 are factors of 10, and 10 appears elsewhere in code)
  • Tiny loss of precision and performance using Math.log(base) etc. instead of Math.LN10
  • In practice will probably only be used for base = 2 and base = e

Overall, I think I would prefer to keep the existing methods as-is and have base-specific variants of d3.ticks and d3.tickIncrement for base = 2 (binary) and base = e (“natural”).

mbostock avatar Nov 23 '21 15:11 mbostock

That's understandable, I can update the pr to have base-specific function variants.

nickofthyme avatar Nov 23 '21 15:11 nickofthyme