Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

optional log base for ticks functions #233

Closed
wants to merge 10 commits into from

Conversation

nickofthyme
Copy link

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 nickofthyme changed the title feat: add optional base to ticks functions optional log base for ticks functions Sep 21, 2021
@nickofthyme
Copy link
Author

@mbostock any feedback on this?

Copy link
Member

@Fil Fil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it! Please also add to the documentation in README.md (in particular, what happens to "1, 2, 5" when the base is not 10).

Another thing that could be tested and documented is what it gives for base e. It surprised me that it switches from units to multiples of e (it makes sense of course, given that 1 is e^0, but I wonder if we have an example of how this can be useful).
https://observablehq.com/d/90d87c2c822df2bd

@nickofthyme
Copy link
Author

nickofthyme commented Oct 27, 2021

@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 nickofthyme requested a review from Fil October 28, 2021 18:20
@nickofthyme
Copy link
Author

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

@nickofthyme
Copy link
Author

@Fil any update on this?

Copy link
Member

@Fil Fil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I had left a comment, but I now find it was waiting in one of many many open tabs… Sorry! For the documentation on Observable, you can fork the notebook and make a suggestion.

README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
Co-authored-by: Philippe Rivière <fil@rezo.net>
@mbostock
Copy link
Member

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”).

@nickofthyme
Copy link
Author

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

Copy link
Author

@nickofthyme nickofthyme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mbostock Sorry for the delay, I finally got around to updating this PR.

I want to follow up on the concerns you mentioned...

return [i1, i2, inc];
}

export default function ticks(start, stop, count) {
export default function ticks(start, stop, count, log) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many positional arguments (start, stop, count, base). 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”).

I'm happy to separate these added methods into separate methods. But I'd like to avoid duplication of code. Would you be ok with having the log option on a base function then exporting a wrapped function for each log type with just start, stop and count args?

So exporting ticks, ticksNatural and ticksBinary, along with their respective tickIncrement functions.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other implicit dependencies on base 10 (e.g., 2 and 5 are factors of 10, and 10 appears elsewhere in code)

Could you point me to the other parts of the code that would need to be addressed?

Comment on lines +5 to +9
const logParams = {
binary: [Math.log2, 2],
natural: [(n) => Math.log1p(n - 1), Math.E],
common: [Math.log10, 10],
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny loss of precision and performance using Math.log(base) etc. instead of Math.LN10

Does this approach work for you to use the respective precise Math methods for each allowed log type?

Comment on lines +6 to +8
binary: [Math.log2, 2],
natural: [(n) => Math.log1p(n - 1), Math.E],
common: [Math.log10, 10],
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice will probably only be used for base = 2 and base = e

Limits the log types to just common, natural and binary.

@nickofthyme nickofthyme requested a review from Fil December 8, 2023 17:29
@nickofthyme nickofthyme deleted the tick-with-base branch February 15, 2024 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

binary ticks increments on linear scale
3 participants