Skip to content

Commit

Permalink
A nice default bin domain.
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Sep 22, 2020
1 parent 5f08e02 commit b32d2d1
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 3 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,10 @@ Like [d3.tickStep](#tickStep), except requires that *start* is always less than

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.

<a name="nice" href="#nice">#</a> d3.<b>nice</b>(<i>start</i>, <i>stop</i>, <i>count</i>)

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*.

<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/master/src/range.js), [Examples](https://observablehq.com/@d3/d3-range)

Returns an array containing an arithmetic progression, similar to the Python built-in [range](http://docs.python.org/library/functions.html#range). This method is often used to iterate over a sequence of uniformly-spaced numeric values, such as the indexes of an array or the ticks of a linear scale. (See also [d3.ticks](#ticks) for nicely-rounded values.)
Expand Down Expand Up @@ -656,6 +660,8 @@ You can then compute the bins from an array of numbers like so:
var bins = bin(numbers);
```

If the default [extent](#extent) domain is used and the [thresholds](#bin_thresholds) are specified as a count (rather than explicit values), then the computed domain will be [niced](#nice) such that all bins are uniform width.

Note that the domain accessor is invoked on the materialized array of [values](#bin_value), not on the input data array.

<a name="bin_thresholds" href="#bin_thresholds">#</a> <i>bin</i>.<b>thresholds</b>([<i>count</i>]) · [Source](https://github.com/d3/d3-array/blob/master/src/bin.js), [Examples](https://observablehq.com/@d3/d3-bin)
Expand Down
6 changes: 5 additions & 1 deletion src/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import bisect from "./bisect.js";
import constant from "./constant.js";
import extent from "./extent.js";
import identity from "./identity.js";
import nice from "./nice.js";
import ticks from "./ticks.js";
import sturges from "./threshold/sturges.js";

Expand All @@ -28,8 +29,11 @@ export default function() {
x1 = xz[1],
tz = threshold(values, x0, x1);

// Convert number of thresholds into uniform thresholds.
// Convert number of thresholds into uniform thresholds,
// and nice the default domain accordingly.
if (!Array.isArray(tz)) {
tz = +tz;
if (domain === extent) [x0, x1] = nice(x0, x1, tz);
tz = ticks(x0, x1, tz);
if (tz[tz.length - 1] === x1) tz.pop(); // exclusive
}
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {default as median} from "./median.js";
export {default as merge} from "./merge.js";
export {default as min} from "./min.js";
export {default as minIndex} from "./minIndex.js";
export {default as nice} from "./nice.js";
export {default as pairs} from "./pairs.js";
export {default as permute} from "./permute.js";
export {default as quantile, quantileSorted} from "./quantile.js";
Expand Down
19 changes: 19 additions & 0 deletions src/nice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {tickIncrement} from "./ticks.js";

export default function nice(start, stop, count) {
let prestep;
while (true) {
const step = tickIncrement(start, stop, count);
if (step === prestep || step === 0 || !isFinite(step)) {
break;
} else if (step > 0) {
start = Math.floor(start / step) * step;
stop = Math.ceil(stop / step) * step;
} else if (step < 0) {
start = Math.ceil(start * step) / step;
stop = Math.floor(stop * step) / step;
}
prestep = step;
}
return [start, stop];
}
4 changes: 2 additions & 2 deletions test/bin-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ tape("bin(data) uses nice thresholds", (test) => {
tape("bin()() returns bins whose rightmost bin is not too wide", (test) => {
const h = d3.bin();
test.deepEqual(h([9.8, 10, 11, 12, 13, 13.2]), [
bin([9.8], 9.8, 10),
bin([9.8], 9, 10),
bin([10], 10, 11),
bin([11], 11, 12),
bin([12], 12, 13),
bin([13, 13.2], 13, 13.2),
bin([13, 13.2], 13, 14),
]);
});

Expand Down
46 changes: 46 additions & 0 deletions test/nice-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const tape = require("tape-await");
const array = require("../");

tape("nice(start, stop, count) returns [start, stop] if any argument is NaN", (test) => {
test.deepEqual(array.nice(NaN, 1, 1), [NaN, 1]);
test.deepEqual(array.nice(0, NaN, 1), [0, NaN]);
test.deepEqual(array.nice(0, 1, NaN), [0, 1]);
test.deepEqual(array.nice(NaN, NaN, 1), [NaN, NaN]);
test.deepEqual(array.nice(0, NaN, NaN), [0, NaN]);
test.deepEqual(array.nice(NaN, 1, NaN), [NaN, 1]);
test.deepEqual(array.nice(NaN, NaN, NaN), [NaN, NaN]);
});

tape("nice(start, stop, count) returns [start, stop] if start === stop", (test) => {
test.deepEqual(array.nice(1, 1, -1), [1, 1]);
test.deepEqual(array.nice(1, 1, 0), [1, 1]);
test.deepEqual(array.nice(1, 1, NaN), [1, 1]);
test.deepEqual(array.nice(1, 1, 1), [1, 1]);
test.deepEqual(array.nice(1, 1, 10), [1, 1]);
});

tape("nice(start, stop, count) returns [start, stop] if count is not positive", (test) => {
test.deepEqual(array.nice(0, 1, -1), [0, 1]);
test.deepEqual(array.nice(0, 1, 0), [0, 1]);
});

tape("nice(start, stop, count) returns [start, stop] if count is infinity", (test) => {
test.deepEqual(array.nice(0, 1, Infinity), [0, 1]);
});

tape("nice(start, stop, count) returns the expected values", (test) => {
test.deepEqual(array.nice(0.132, 0.876, 1000), [0.132, 0.876]);
test.deepEqual(array.nice(0.132, 0.876, 100), [0.13, 0.88]);
test.deepEqual(array.nice(0.132, 0.876, 30), [0.12, 0.88]);
test.deepEqual(array.nice(0.132, 0.876, 10), [0.1, 0.9]);
test.deepEqual(array.nice(0.132, 0.876, 6), [0.1, 0.9]);
test.deepEqual(array.nice(0.132, 0.876, 5), [0, 1]);
test.deepEqual(array.nice(0.132, 0.876, 1), [0, 1]);
test.deepEqual(array.nice(132, 876, 1000), [132, 876]);
test.deepEqual(array.nice(132, 876, 100), [130, 880]);
test.deepEqual(array.nice(132, 876, 30), [120, 880]);
test.deepEqual(array.nice(132, 876, 10), [100, 900]);
test.deepEqual(array.nice(132, 876, 6), [100, 900]);
test.deepEqual(array.nice(132, 876, 5), [0, 1000]);
test.deepEqual(array.nice(132, 876, 1), [0, 1000]);
});
62 changes: 62 additions & 0 deletions test/tickIncrement-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const tape = require("tape-await");
const array = require("../");

tape("tickIncrement(start, stop, count) returns NaN if any argument is NaN", (test) => {
test.ok(isNaN(array.tickIncrement(NaN, 1, 1)));
test.ok(isNaN(array.tickIncrement(0, NaN, 1)));
test.ok(isNaN(array.tickIncrement(0, 1, NaN)));
test.ok(isNaN(array.tickIncrement(NaN, NaN, 1)));
test.ok(isNaN(array.tickIncrement(0, NaN, NaN)));
test.ok(isNaN(array.tickIncrement(NaN, 1, NaN)));
test.ok(isNaN(array.tickIncrement(NaN, NaN, NaN)));
});

tape("tickIncrement(start, stop, count) returns NaN or -Infinity if start === stop", (test) => {
test.ok(isNaN(array.tickIncrement(1, 1, -1)));
test.ok(isNaN(array.tickIncrement(1, 1, 0)));
test.ok(isNaN(array.tickIncrement(1, 1, NaN)));
test.equal(array.tickIncrement(1, 1, 1), -Infinity);
test.equal(array.tickIncrement(1, 1, 10), -Infinity);
});

tape("tickIncrement(start, stop, count) returns 0 or Infinity if count is not positive", (test) => {
test.equal(array.tickIncrement(0, 1, -1), Infinity);
test.equal(array.tickIncrement(0, 1, 0), Infinity);
});

tape("tickIncrement(start, stop, count) returns -Infinity if count is infinity", (test) => {
test.equal(array.tickIncrement(0, 1, Infinity), -Infinity);
});

tape("tickIncrement(start, stop, count) returns approximately count + 1 tickIncrement when start < stop", (test) => {
test.equal(array.tickIncrement( 0, 1, 10), -10);
test.equal(array.tickIncrement( 0, 1, 9), -10);
test.equal(array.tickIncrement( 0, 1, 8), -10);
test.equal(array.tickIncrement( 0, 1, 7), -5);
test.equal(array.tickIncrement( 0, 1, 6), -5);
test.equal(array.tickIncrement( 0, 1, 5), -5);
test.equal(array.tickIncrement( 0, 1, 4), -5);
test.equal(array.tickIncrement( 0, 1, 3), -2);
test.equal(array.tickIncrement( 0, 1, 2), -2);
test.equal(array.tickIncrement( 0, 1, 1), 1);
test.equal(array.tickIncrement( 0, 10, 10), 1);
test.equal(array.tickIncrement( 0, 10, 9), 1);
test.equal(array.tickIncrement( 0, 10, 8), 1);
test.equal(array.tickIncrement( 0, 10, 7), 2);
test.equal(array.tickIncrement( 0, 10, 6), 2);
test.equal(array.tickIncrement( 0, 10, 5), 2);
test.equal(array.tickIncrement( 0, 10, 4), 2);
test.equal(array.tickIncrement( 0, 10, 3), 5);
test.equal(array.tickIncrement( 0, 10, 2), 5);
test.equal(array.tickIncrement( 0, 10, 1), 10);
test.equal(array.tickIncrement(-10, 10, 10), 2);
test.equal(array.tickIncrement(-10, 10, 9), 2);
test.equal(array.tickIncrement(-10, 10, 8), 2);
test.equal(array.tickIncrement(-10, 10, 7), 2);
test.equal(array.tickIncrement(-10, 10, 6), 5);
test.equal(array.tickIncrement(-10, 10, 5), 5);
test.equal(array.tickIncrement(-10, 10, 4), 5);
test.equal(array.tickIncrement(-10, 10, 3), 5);
test.equal(array.tickIncrement(-10, 10, 2), 10);
test.equal(array.tickIncrement(-10, 10, 1), 20);
});

0 comments on commit b32d2d1

Please sign in to comment.