Repository: d3/d3-array
Pull request #264 (2022-12-26 18:31:40)

try to generate at least one tick
I noticed that this returns zero ticks:

```js
d3.ticks(1, 364, 1.1)
```

That’s because the chosen tick step is 500, and of course neither 0 or 500 is within [1, 364]. A more egregious case which also returns the empty array for the same reasons is:

```js
d3.ticks(1, 499, 1.5)
```

The fix is simply to double the number of requested ticks recursively when the actual number of ticks is zero. (That’s because tickIncrement does not take into account the alignment of the domain with the tick values.) To avoid any risk of infinite recursion, we only double the count when the count is in the range [0.5, 2); at most two doublings as possible.

This means that this:

```js
d3.ticks(1, 499, 0.5)
```

likewise now returns `[200, 400]` instead of `[]`, but that seems consistent with the documented behavior of returning *count* + 1 = 1.5 values.

mbostock: Should we do this inside of tickIncrement instead of ticks so that the two are guaranteed to be consistent?

Patch:
diff --git a/src/ticks.js b/src/ticks.js
index 6ce6977..5868d8d 100644
--- a/src/ticks.js
+++ b/src/ticks.js
@@ -1,54 +1,55 @@
-var e10 = Math.sqrt(50),
+const e10 = Math.sqrt(50),
     e5 = Math.sqrt(10),
     e2 = Math.sqrt(2);
 
-export default function ticks(start, stop, count) {
-  var reverse,
-      i = -1,
-      n,
-      ticks,
-      step;
+function tickSpec(start, stop, count) {
+  const step = (stop - start) / Math.max(0, count),
+      power = Math.floor(Math.log10(step)),
+      error = step / Math.pow(10, power),
+      factor = error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1;
+  let i1, i2, inc;
+  if (power < 0) {
+    inc = Math.pow(10, -power) / factor;
+    i1 = Math.round(start * inc);
+    i2 = Math.round(stop * inc);
+    if (i1 / inc < start) ++i1;
+    if (i2 / inc > stop) --i2;
+    inc = -inc;
+  } else {
+    inc = Math.pow(10, power) * factor;
+    i1 = Math.round(start / inc);
+    i2 = Math.round(stop / inc);
+    if (i1 * inc < start) ++i1;
+    if (i2 * inc > stop) --i2;
+  }
+  if (i2 < i1 && 0.5 <= count && count < 2) return tickSpec(start, stop, count * 2);
+  return [i1, i2, inc];
+}
 
+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 > 0) {
-    let r0 = Math.round(start / step), r1 = Math.round(stop / step);
-    if (r0 * step < start) ++r0;
-    if (r1 * step > stop) --r1;
-    ticks = new Array(n = r1 - r0 + 1);
-    while (++i < n) ticks[i] = (r0 + i) * step;
+  if (!(count > 0)) return [];
+  if (start === stop) return [start];
+  const reverse = stop < start, [i1, i2, inc] = reverse ? tickSpec(stop, start, count) : tickSpec(start, stop, count);
+  if (!(i2 >= i1)) return [];
+  const n = i2 - i1 + 1, ticks = new Array(n);
+  if (reverse) {
+    if (inc < 0) for (let i = 0; i < n; ++i) ticks[i] = (i2 - i) / -inc;
+    else for (let i = 0; i < n; ++i) ticks[i] = (i2 - i) * inc;
   } else {
-    step = -step;
-    let r0 = Math.round(start * step), r1 = Math.round(stop * step);
-    if (r0 / step < start) ++r0;
-    if (r1 / step > stop) --r1;
-    ticks = new Array(n = r1 - r0 + 1);
-    while (++i < n) ticks[i] = (r0 + i) / step;
+    if (inc < 0) for (let i = 0; i < n; ++i) ticks[i] = (i1 + i) / -inc;
+    else for (let i = 0; i < n; ++i) ticks[i] = (i1 + i) * inc;
   }
-
-  if (reverse) ticks.reverse();
-
   return ticks;
 }
 
 export function tickIncrement(start, stop, count) {
-  var step = (stop - start) / Math.max(0, count),
-      power = Math.floor(Math.log(step) / Math.LN10),
-      error = step / Math.pow(10, 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);
+  stop = +stop, start = +start, count = +count;
+  return tickSpec(start, stop, count)[2];
 }
 
 export function tickStep(start, stop, count) {
-  var step0 = Math.abs(stop - start) / Math.max(0, count),
-      step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)),
-      error = step0 / step1;
-  if (error >= e10) step1 *= 10;
-  else if (error >= e5) step1 *= 5;
-  else if (error >= e2) step1 *= 2;
-  return stop < start ? -step1 : step1;
+  stop = +stop, start = +start, count = +count;
+  const reverse = stop < start, inc = reverse ? tickIncrement(stop, start, count) : tickIncrement(start, stop, count);
+  return (reverse ? -1 : 1) * (inc < 0 ? 1 / -inc : inc);
 }


Test patch:
diff --git a/test/ticks-test.js b/test/ticks-test.js
index 3b4fd98..4793e65 100644
--- a/test/ticks-test.js
+++ b/test/ticks-test.js
@@ -109,3 +109,19 @@ it("ticks(start, stop, count) returns the reverse of ticks(stop, start, count)",
 it("ticks(start, stop, count) handles precision problems", () => {
   assert.deepStrictEqual(ticks(0.98, 1.14, 10), [0.98, 1, 1.02, 1.04, 1.06, 1.08, 1.1, 1.12, 1.14]);
 });
+
+it("ticks(start, stop, count) tries to return at least one tick if count >= 0.5", () => {
+  assert.deepStrictEqual(ticks(1, 364, 0.1), []);
+  assert.deepStrictEqual(ticks(1, 364, 0.499), []);
+  assert.deepStrictEqual(ticks(1, 364, 0.5), [200]);
+  assert.deepStrictEqual(ticks(1, 364, 1), [200]);
+  assert.deepStrictEqual(ticks(1, 364, 1.5), [200]);
+  assert.deepStrictEqual(ticks(1, 499, 1), [200, 400]);
+  assert.deepStrictEqual(ticks(364, 1, 0.5), [200]);
+  assert.deepStrictEqual(ticks(0.001, 0.364, 0.5), [0.2]);
+  assert.deepStrictEqual(ticks(0.364, 0.001, 0.5), [0.2]);
+  assert.deepStrictEqual(ticks(-1, -364, 0.5), [-200]);
+  assert.deepStrictEqual(ticks(-364, -1, 0.5), [-200]);
+  assert.deepStrictEqual(ticks(-0.001, -0.364, 0.5), [-0.2]);
+  assert.deepStrictEqual(ticks(-0.364, -0.001, 0.5), [-0.2]);
+});