diff --git a/README.md b/README.md
index dceea5b4..f9dee700 100644
--- a/README.md
+++ b/README.md
@@ -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.
+# d3.nice(start, stop, count)
+
+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*.
+
# d3.range([start, ]stop[, step]) · [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.)
@@ -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.
# bin.thresholds([count]) · [Source](https://github.com/d3/d3-array/blob/master/src/bin.js), [Examples](https://observablehq.com/@d3/d3-bin)
diff --git a/src/bin.js b/src/bin.js
index ea69ae56..62ff5ca2 100644
--- a/src/bin.js
+++ b/src/bin.js
@@ -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";
@@ -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
}
diff --git a/src/index.js b/src/index.js
index 449f608b..92d41d77 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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";
diff --git a/src/nice.js b/src/nice.js
new file mode 100644
index 00000000..378243a2
--- /dev/null
+++ b/src/nice.js
@@ -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];
+}
diff --git a/test/bin-test.js b/test/bin-test.js
index 5243c70f..b1b6b766 100644
--- a/test/bin-test.js
+++ b/test/bin-test.js
@@ -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),
]);
});
diff --git a/test/nice-test.js b/test/nice-test.js
new file mode 100644
index 00000000..7dd5a49e
--- /dev/null
+++ b/test/nice-test.js
@@ -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]);
+});
diff --git a/test/tickIncrement-test.js b/test/tickIncrement-test.js
new file mode 100644
index 00000000..c14c0fb1
--- /dev/null
+++ b/test/tickIncrement-test.js
@@ -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);
+});