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
14 changes: 7 additions & 7 deletions README.md
Expand Up @@ -632,23 +632,23 @@ const shuffle = d3.shuffler(random);
shuffle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); // returns [7, 4, 5, 3, 9, 0, 6, 1, 2, 8]
```

<a name="ticks" href="#ticks">#</a> d3.<b>ticks</b>(<i>start</i>, <i>stop</i>, <i>count</i>) · [Source](https://github.com/d3/d3-array/blob/main/src/ticks.js), [Examples](https://observablehq.com/@d3/d3-ticks)
<a name="ticks" href="#ticks">#</a> d3.<b>ticks</b>(<i>start</i>, <i>stop</i>, <i>count</i>, <i>base</i>) · [Source](https://github.com/d3/d3-array/blob/main/src/ticks.js), [Examples](https://observablehq.com/@d3/d3-ticks)

Returns an array of approximately *count* + 1 uniformly-spaced, nicely-rounded values between *start* and *stop* (inclusive). Each value is a power of ten multiplied by 1, 2 or 5. See also [d3.tickIncrement](#tickIncrement), [d3.tickStep](#tickStep) and [*linear*.ticks](https://github.com/d3/d3-scale/blob/main/README.md#linear_ticks).
Returns an array of approximately *count* + 1 uniformly-spaced, nicely-rounded values between *start* and *stop* (inclusive). Each value is a power of the *base* (default is 10) multiplied by 1, 2 or 5. See also [d3.tickIncrement](#tickIncrement), [d3.tickStep](#tickStep) and [*linear*.ticks](https://github.com/d3/d3-scale/blob/main/README.md#linear_ticks).

Ticks are inclusive in the sense that they may include the specified *start* and *stop* values if (and only if) they are exact, nicely-rounded values consistent with the inferred [step](#tickStep). More formally, each returned tick *t* satisfies *start* ≤ *t* and *t* ≤ *stop*.

<a name="tickIncrement" href="#tickIncrement">#</a> d3.<b>tickIncrement</b>(<i>start</i>, <i>stop</i>, <i>count</i>) · [Source](https://github.com/d3/d3-array/blob/main/src/ticks.js), [Examples](https://observablehq.com/@d3/d3-ticks)
<a name="tickIncrement" href="#tickIncrement">#</a> d3.<b>tickIncrement</b>(<i>start</i>, <i>stop</i>, <i>count</i>, <i>base</i>) · [Source](https://github.com/d3/d3-array/blob/main/src/ticks.js), [Examples](https://observablehq.com/@d3/d3-ticks)

Like [d3.tickStep](#tickStep), except requires that *start* is always less than or equal to *stop*, and if the tick step for the given *start*, *stop* and *count* would be less than one, returns the negative inverse tick step instead. This method is always guaranteed to return an integer, and is used by [d3.ticks](#ticks) to guarantee that the returned tick values are represented as precisely as possible in IEEE 754 floating point.

<a name="tickStep" href="#tickStep">#</a> d3.<b>tickStep</b>(<i>start</i>, <i>stop</i>, <i>count</i>) · [Source](https://github.com/d3/d3-array/blob/main/src/ticks.js), [Examples](https://observablehq.com/@d3/d3-ticks)
<a name="tickStep" href="#tickStep">#</a> d3.<b>tickStep</b>(<i>start</i>, <i>stop</i>, <i>count</i>, <i>base</i>) · [Source](https://github.com/d3/d3-array/blob/main/src/ticks.js), [Examples](https://observablehq.com/@d3/d3-ticks)

Returns the difference between adjacent tick values if the same arguments were passed to [d3.ticks](#ticks): a nicely-rounded value that is a power of ten multiplied by 1, 2 or 5. Note that due to the limited precision of IEEE 754 floating point, the returned value may not be exact decimals; use [d3-format](https://github.com/d3/d3-format) to format numbers for human consumption.
Returns the difference between adjacent tick values if the same arguments were passed to [d3.ticks](#ticks): a nicely-rounded value that is a power of the *base* (default is 10) multiplied by 1, 2 or 5. Note that due to the limited precision of IEEE 754 floating point, the returned value may not be exact decimals; use [d3-format](https://github.com/d3/d3-format) to format numbers for human consumption.

<a name="nice" href="#nice">#</a> d3.<b>nice</b>(<i>start</i>, <i>stop</i>, <i>count</i>) · [Source](https://github.com/d3/d3-array/blob/main/src/nice.js)
<a name="nice" href="#nice">#</a> d3.<b>nice</b>(<i>start</i>, <i>stop</i>, <i>count</i>, <i>base</i>) · [Source](https://github.com/d3/d3-array/blob/main/src/nice.js)

Returns a new interval [*niceStart*, *niceStop*] covering the given interval [*start*, *stop*] and where *niceStart* and *niceStop* are guaranteed to align with the corresponding [tick step](#tickStep). Like [d3.tickIncrement](#tickIncrement), this requires that *start* is less than or equal to *stop*.
Returns a new interval [*niceStart*, *niceStop*] covering the given interval [*start*, *stop*] and where *niceStart* and *niceStop* are guaranteed to align with the corresponding [tick step](#tickStep) given the provided *base* (default is 10). Like [d3.tickIncrement](#tickIncrement), this requires that *start* is less than or equal to *stop*.

<a name="range" href="#range">#</a> d3.<b>range</b>([<i>start</i>, ]<i>stop</i>[, <i>step</i>]) · [Source](https://github.com/d3/d3-array/blob/main/src/range.js), [Examples](https://observablehq.com/@d3/d3-range)

Expand Down
4 changes: 2 additions & 2 deletions src/nice.js
@@ -1,9 +1,9 @@
import {tickIncrement} from "./ticks.js";

export default function nice(start, stop, count) {
export default function nice(start, stop, count, base) {
let prestep;
while (true) {
const step = tickIncrement(start, stop, count);
const step = tickIncrement(start, stop, count, base);
if (step === prestep || step === 0 || !isFinite(step)) {
return [start, stop];
} else if (step > 0) {
Expand Down
18 changes: 9 additions & 9 deletions src/ticks.js
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?

Expand Up @@ -2,7 +2,7 @@ var e10 = Math.sqrt(50),
e5 = Math.sqrt(10),
e2 = Math.sqrt(2);

export default function ticks(start, stop, count) {
export default function ticks(start, stop, count, base = 10) {
var reverse,
i = -1,
n,
Expand All @@ -12,7 +12,7 @@ export default function ticks(start, stop, count) {
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 = tickIncrement(start, stop, count, base)) === 0 || !isFinite(step)) return [];

if (step > 0) {
let r0 = Math.round(start / step), r1 = Math.round(stop / step);
Expand All @@ -34,18 +34,18 @@ export default function ticks(start, stop, count) {
return ticks;
}

export function tickIncrement(start, stop, count) {
export function tickIncrement(start, stop, count, base = 10) {
var step = (stop - start) / Math.max(0, count),
power = Math.floor(Math.log(step) / Math.LN10),
error = step / Math.pow(10, power);
power = Math.floor(Math.log(step) / Math.log(base) + Number.EPSILON),
error = step / Math.pow(base, power);
return power >= 0
? (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1) * Math.pow(10, power)
: -Math.pow(10, -power) / (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1);
? (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);
}

export function tickStep(start, stop, count) {
export function tickStep(start, stop, count, base = 10) {
var step0 = Math.abs(stop - start) / Math.max(0, count),
step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)),
step1 = Math.pow(base, Math.floor(Math.log(step0) / Math.log(base) + Number.EPSILON)),
error = step0 / step1;
if (error >= e10) step1 *= 10;
else if (error >= e5) step1 *= 5;
Expand Down
21 changes: 21 additions & 0 deletions test/nice-test.js
Expand Up @@ -44,3 +44,24 @@ it("nice(start, stop, count) returns the expected values", () => {
assert.deepStrictEqual(nice(132, 876, 5), [0, 1000]);
assert.deepStrictEqual(nice(132, 876, 1), [0, 1000]);
});

it("nice(start, stop, count, base) returns the expected values with base 2", () => {
assert.deepStrictEqual(nice(0.126, 0.51, 32, 2), [0.125, 0.515625]);
assert.deepStrictEqual(nice(0.126, 0.48, 10, 2), [0.125, 0.5]);
assert.deepStrictEqual(nice(0.126, 0.48, 6, 2), [0.125, 0.5]);
assert.deepStrictEqual(nice(0.126, 0.48, 5, 2), [0.125, 0.5]);
assert.deepStrictEqual(nice(0.126, 0.48, 1, 2), [0, 0.5]);
assert.deepStrictEqual(nice(129, 876, 1000, 2), [129, 876]);
assert.deepStrictEqual(nice(129, 876, 100, 2), [128, 880]);
assert.deepStrictEqual(nice(129, 876, 30, 2), [128, 896]);
assert.deepStrictEqual(nice(129, 876, 10, 2), [128, 896]);
assert.deepStrictEqual(nice(129, 876, 6, 2), [128, 896]);
assert.deepStrictEqual(nice(129, 876, 5, 2), [128, 896]);
assert.deepStrictEqual(nice(129, 876, 1, 2), [0, 1024]);
});

it("nice(start, stop, count, base) returns the expected values with base e", () => {
assert.deepStrictEqual(nice(Math.E + 0.1, Math.E * 3 + 0.1, 2, Math.E), [0, Math.E * 4]);
assert.deepStrictEqual(nice(-Math.E - 0.1, Math.E + 0.1, 2, Math.E), [-Math.E * 2, Math.E * 2]);
assert.deepStrictEqual(nice(-Math.E * 10 - 0.5, Math.E * 20 - 0.5, 30, Math.E), [-Math.E * 11, Math.E * 20]);
});
128 changes: 128 additions & 0 deletions test/tickIncrement-test.js
Expand Up @@ -60,3 +60,131 @@ it("tickIncrement(start, stop, count) returns approximately count + 1 tickIncrem
assert.strictEqual(tickIncrement(-10, 10, 2), 10);
assert.strictEqual(tickIncrement(-10, 10, 1), 20);
});


describe("tickIncrement(start, stop, count, base) returns approximately count + 1 tickIncrement when start < stop", () => {
it("base 2", () => {
assert.strictEqual(tickIncrement( 0, 1, 10, 2), -8);
assert.strictEqual(tickIncrement( 0, 1, 9, 2), -8);
assert.strictEqual(tickIncrement( 0, 1, 8, 2), -8);
assert.strictEqual(tickIncrement( 0, 1, 7, 2), -8);
assert.strictEqual(tickIncrement( 0, 1, 6, 2), -8);
assert.strictEqual(tickIncrement( 0, 1, 5, 2), -4);
assert.strictEqual(tickIncrement( 0, 1, 4, 2), -4);
assert.strictEqual(tickIncrement( 0, 1, 3, 2), -4);
assert.strictEqual(tickIncrement( 0, 1, 2, 2), -2);
assert.strictEqual(tickIncrement( 0, 1, 1, 2), 1);
assert.strictEqual(tickIncrement( 0, 10, 10, 2), 1);
assert.strictEqual(tickIncrement( 0, 10, 9, 2), 1);
assert.strictEqual(tickIncrement( 0, 10, 8, 2), 1);
assert.strictEqual(tickIncrement( 0, 10, 7, 2), 2);
assert.strictEqual(tickIncrement( 0, 10, 6, 2), 2);
assert.strictEqual(tickIncrement( 0, 10, 5, 2), 2);
assert.strictEqual(tickIncrement( 0, 10, 4, 2), 2);
assert.strictEqual(tickIncrement( 0, 10, 3, 2), 4);
assert.strictEqual(tickIncrement( 0, 10, 2, 2), 4);
assert.strictEqual(tickIncrement( 0, 10, 1, 2), 8);
assert.strictEqual(tickIncrement(-10, 10, 10, 2), 2);
assert.strictEqual(tickIncrement(-10, 10, 9, 2), 2);
assert.strictEqual(tickIncrement(-10, 10, 8, 2), 2);
assert.strictEqual(tickIncrement(-10, 10, 7, 2), 4);
assert.strictEqual(tickIncrement(-10, 10, 6, 2), 4);
assert.strictEqual(tickIncrement(-10, 10, 5, 2), 4);
assert.strictEqual(tickIncrement(-10, 10, 4, 2), 4);
assert.strictEqual(tickIncrement(-10, 10, 3, 2), 8);
assert.strictEqual(tickIncrement(-10, 10, 2, 2), 8);
assert.strictEqual(tickIncrement(-10, 10, 1, 2), 16);
});

it("base 5", () => {
assert.strictEqual(tickIncrement( 0, 1, 10, 5), -12.5);
assert.strictEqual(tickIncrement( 0, 1, 9, 5), -12.5);
assert.strictEqual(tickIncrement( 0, 1, 8, 5), -12.5);
assert.strictEqual(tickIncrement( 0, 1, 7, 5), -5);
assert.strictEqual(tickIncrement( 0, 1, 6, 5), -5);
assert.strictEqual(tickIncrement( 0, 1, 5, 5), -5);
assert.strictEqual(tickIncrement( 0, 1, 4, 5), -5);
assert.strictEqual(tickIncrement( 0, 1, 3, 5), -2.5);
assert.strictEqual(tickIncrement( 0, 1, 2, 5), -2.5);
assert.strictEqual(tickIncrement( 0, 1, 1, 5), 1);
assert.strictEqual(tickIncrement( 0, 10, 10, 5), 1);
assert.strictEqual(tickIncrement( 0, 10, 9, 5), 1);
assert.strictEqual(tickIncrement( 0, 10, 8, 5), 1);
assert.strictEqual(tickIncrement( 0, 10, 7, 5), 2);
assert.strictEqual(tickIncrement( 0, 10, 6, 5), 2);
assert.strictEqual(tickIncrement( 0, 10, 5, 5), 2);
assert.strictEqual(tickIncrement( 0, 10, 4, 5), 2);
assert.strictEqual(tickIncrement( 0, 10, 3, 5), 5);
assert.strictEqual(tickIncrement( 0, 10, 2, 5), 5);
assert.strictEqual(tickIncrement( 0, 10, 1, 5), 10);
assert.strictEqual(tickIncrement(-10, 10, 10, 5), 2);
assert.strictEqual(tickIncrement(-10, 10, 9, 5), 2);
assert.strictEqual(tickIncrement(-10, 10, 8, 5), 2);
assert.strictEqual(tickIncrement(-10, 10, 7, 5), 2);
assert.strictEqual(tickIncrement(-10, 10, 6, 5), 5);
assert.strictEqual(tickIncrement(-10, 10, 5, 5), 5);
assert.strictEqual(tickIncrement(-10, 10, 4, 5), 5);
assert.strictEqual(tickIncrement(-10, 10, 3, 5), 5);
assert.strictEqual(tickIncrement(-10, 10, 2, 5), 10);
assert.strictEqual(tickIncrement(-10, 10, 1, 5), 25);
});

it("base 10", () => {
assert.strictEqual(tickIncrement( 0, 1, 10, 10), -10);
assert.strictEqual(tickIncrement( 0, 1, 9, 10), -10);
assert.strictEqual(tickIncrement( 0, 1, 8, 10), -10);
assert.strictEqual(tickIncrement( 0, 1, 7, 10), -5);
assert.strictEqual(tickIncrement( 0, 1, 6, 10), -5);
assert.strictEqual(tickIncrement( 0, 1, 5, 10), -5);
assert.strictEqual(tickIncrement( 0, 1, 4, 10), -5);
assert.strictEqual(tickIncrement( 0, 1, 3, 10), -2);
assert.strictEqual(tickIncrement( 0, 1, 2, 10), -2);
assert.strictEqual(tickIncrement( 0, 1, 1, 10), 1);
assert.strictEqual(tickIncrement( 0, 10, 10, 10), 1);
assert.strictEqual(tickIncrement( 0, 10, 9, 10), 1);
assert.strictEqual(tickIncrement( 0, 10, 8, 10), 1);
assert.strictEqual(tickIncrement( 0, 10, 7, 10), 2);
assert.strictEqual(tickIncrement( 0, 10, 6, 10), 2);
assert.strictEqual(tickIncrement( 0, 10, 5, 10), 2);
assert.strictEqual(tickIncrement( 0, 10, 4, 10), 2);
assert.strictEqual(tickIncrement( 0, 10, 3, 10), 5);
assert.strictEqual(tickIncrement( 0, 10, 2, 10), 5);
assert.strictEqual(tickIncrement( 0, 10, 1, 10), 10);
assert.strictEqual(tickIncrement(-10, 10, 10, 10), 2);
assert.strictEqual(tickIncrement(-10, 10, 9, 10), 2);
assert.strictEqual(tickIncrement(-10, 10, 8, 10), 2);
assert.strictEqual(tickIncrement(-10, 10, 7, 10), 2);
assert.strictEqual(tickIncrement(-10, 10, 6, 10), 5);
assert.strictEqual(tickIncrement(-10, 10, 5, 10), 5);
assert.strictEqual(tickIncrement(-10, 10, 4, 10), 5);
assert.strictEqual(tickIncrement(-10, 10, 3, 10), 5);
assert.strictEqual(tickIncrement(-10, 10, 2, 10), 10);
assert.strictEqual(tickIncrement(-10, 10, 1, 10), 20);
});

it("base e", () => {
assert.strictEqual(normalizeLn(tickIncrement( 0, Math.E / 100, 10, Math.E)), -1);
assert.strictEqual(normalizeLn(tickIncrement( 0, Math.E / 10, 10, Math.E)), -0.5);
assert.strictEqual(normalizeLn(tickIncrement( 0, Math.E / 10, 1, Math.E)), -0.5);
assert.strictEqual(normalizeLn(tickIncrement( 0, Math.E, 3, Math.E)), -0.5);
assert.strictEqual(normalizeLn(tickIncrement( -Math.E, Math.E, 2, Math.E)), 1);
assert.strictEqual(normalizeLn(tickIncrement( 0, Math.E * 10, 10, Math.E)), 1);
assert.strictEqual(normalizeLn(tickIncrement( 1, Math.E * 10, 10, Math.E)), 2);
assert.strictEqual(normalizeLn(tickIncrement( -Math.E * 10, Math.E * 10, 10, Math.E)), 2);
assert.strictEqual(normalizeLn(tickIncrement( Math.E, Math.E * 100, 10, Math.E)), 1);
assert.strictEqual(normalizeLn(tickIncrement( -Math.E * 100, Math.E * 100, 10, Math.E)), 2);
assert.strictEqual(normalizeLn(tickIncrement( 0, Math.E * 100, 10, Math.E)), 1);
assert.strictEqual(tickIncrement( 0, 10, 10, Math.E), 1);
assert.strictEqual(tickIncrement( -10, 10, 10, Math.E), 2);
});
})

export function normalizeLn(value) {
let tries = 0, normValue = value;
while (tries++ < 10 && normValue % 0.5 !== 0) normValue *= Math.E;
if (tries > 10) {
tries = 0, normValue = value
while (tries++ < 10 && normValue % 0.5 !== 0) normValue /= Math.E;
}
return tries > 10 ? NaN : normValue;
}