Skip to content

Commit

Permalink
null, undefined, NaN and -Infinity create holes (#61)
Browse files Browse the repository at this point in the history
* null, undefined, NaN and -Infinity create holes. Ignore infinite values when computing the thresholds.
closes #49

* Update src/contours.js

Co-authored-by: Mike Bostock <[email protected]>

* post-review

* revert unrelated style changes

* nullish

* extract closure

* a few comments

* another comment

* another comment

Co-authored-by: Mike Bostock <[email protected]>
  • Loading branch information
Fil and mbostock authored Jan 8, 2023
1 parent 517b36c commit f81b2b6
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 16 deletions.
55 changes: 39 additions & 16 deletions src/contours.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function() {

// Convert number of thresholds into uniform thresholds.
if (!Array.isArray(tz)) {
const e = extent(values), ts = tickStep(e[0], e[1], tz);
const e = extent(values, finite), ts = tickStep(e[0], e[1], tz);
tz = ticks(Math.floor(e[0] / ts) * ts, Math.floor(e[1] / ts - 1) * ts, tz);
} else {
tz = tz.slice().sort(ascending);
Expand All @@ -48,11 +48,14 @@ export default function() {
// Accumulate, smooth contour rings, assign holes to exterior rings.
// Based on https://github.com/mbostock/shapefile/blob/v0.6.2/shp/polygon.js
function contour(values, value) {
const v = value == null ? NaN : +value;
if (isNaN(v)) throw new Error(`invalid value: ${value}`);

var polygons = [],
holes = [];

isorings(values, value, function(ring) {
smooth(ring, values, value);
isorings(values, v, function(ring) {
smooth(ring, values, v);
if (area(ring) > 0) polygons.push([ring]);
else holes.push(ring);
});
Expand Down Expand Up @@ -82,23 +85,23 @@ export default function() {

// Special case for the first row (y = -1, t2 = t3 = 0).
x = y = -1;
t1 = values[0] >= value;
t1 = above(values[0], value);
cases[t1 << 1].forEach(stitch);
while (++x < dx - 1) {
t0 = t1, t1 = values[x + 1] >= value;
t0 = t1, t1 = above(values[x + 1], value);
cases[t0 | t1 << 1].forEach(stitch);
}
cases[t1 << 0].forEach(stitch);

// General case for the intermediate rows.
while (++y < dy - 1) {
x = -1;
t1 = values[y * dx + dx] >= value;
t2 = values[y * dx] >= value;
t1 = above(values[y * dx + dx], value);
t2 = above(values[y * dx], value);
cases[t1 << 1 | t2 << 2].forEach(stitch);
while (++x < dx - 1) {
t0 = t1, t1 = values[y * dx + dx + x + 1] >= value;
t3 = t2, t2 = values[y * dx + x + 1] >= value;
t0 = t1, t1 = above(values[y * dx + dx + x + 1], value);
t3 = t2, t2 = above(values[y * dx + x + 1], value);
cases[t0 | t1 << 1 | t2 << 2 | t3 << 3].forEach(stitch);
}
cases[t1 | t2 << 3].forEach(stitch);
Expand All @@ -109,7 +112,7 @@ export default function() {
t2 = values[y * dx] >= value;
cases[t2 << 2].forEach(stitch);
while (++x < dx - 1) {
t3 = t2, t2 = values[y * dx + x + 1] >= value;
t3 = t2, t2 = above(values[y * dx + x + 1], value);
cases[t2 << 2 | t3 << 3].forEach(stitch);
}
cases[t2 << 3].forEach(stitch);
Expand Down Expand Up @@ -166,15 +169,12 @@ export default function() {
y = point[1],
xt = x | 0,
yt = y | 0,
v0,
v1 = values[yt * dx + xt];
v1 = valid(values[yt * dx + xt]);
if (x > 0 && x < dx && xt === x) {
v0 = values[yt * dx + xt - 1];
point[0] = x + (value - v0) / (v1 - v0) - 0.5;
point[0] = smooth1(x, valid(values[yt * dx + xt - 1]), v1, value);
}
if (y > 0 && y < dy && yt === y) {
v0 = values[(yt - 1) * dx + xt];
point[1] = y + (value - v0) / (v1 - v0) - 0.5;
point[1] = smooth1(y, valid(values[(yt - 1) * dx + xt]), v1, value);
}
});
}
Expand All @@ -198,3 +198,26 @@ export default function() {

return contours;
}

// When computing the extent, ignore infinite values (as well as invalid ones).
function finite(x) {
return isFinite(x) ? x : NaN;
}

// Is the (possibly invalid) x greater than or equal to the (known valid) value?
// Treat any invalid value as below negative infinity.
function above(x, value) {
return x == null ? false : +x >= value;
}

// During smoothing, treat any invalid value as negative infinity.
function valid(v) {
return v == null || isNaN(v = +v) ? -Infinity : v;
}

function smooth1(x, v0, v1, value) {
const a = value - v0;
const b = v1 - v0;
const d = isFinite(a) || isFinite(b) ? a / b : Math.sign(a) / Math.sign(b);
return isNaN(d) ? x : x + d - 0.5;
}
66 changes: 66 additions & 0 deletions test/contours-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,69 @@ it("contours(values) returns the expected thresholds", () => {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]).map(d => d.value), [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95]);
});

it("contours(values) ignores infinite values when computing the thresholds", () => {
const c = contours().size([10, 10]).thresholds(20);
assert.deepStrictEqual(c([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, -Infinity, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 0, 1, 1, 1, 0, 0,
0, 1, 0, 1, 0, 1, 0, 1, 0, 0,
0, 1, 1, 1, 0, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, Infinity, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]).map(d => d.value), [0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95]);
});

it("contours(values) treats null, undefined, NaN and -Infinity as holes", () => {
const c = contours().size([10, 10]);
assert.deepStrictEqual(c.contour([
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, -Infinity, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, null, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 2, 2, 2, 1,
1, 1, NaN, 1, 1, 1, 2, -Infinity, 2, 1,
1, 1, 1, 1, 1, 1, 2, 2, 2, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1
], 0), {"type":"MultiPolygon","value":0,"coordinates":[[[[10,9.5],[10,8.5],[10,7.5],[10,6.5],[10,5.5],[10,4.5],[10,3.5],[10,2.5],[10,1.5],[10,0.5],[9.5,0],[8.5,0],[7.5,0],[6.5,0],[5.5,0],[4.5,0],[3.5,0],[2.5,0],[1.5,0],[0.5,0],[0,0.5],[0,1.5],[0,2.5],[0,3.5],[0,4.5],[0,5.5],[0,6.5],[0,7.5],[0,8.5],[0,9.5],[0.5,10],[1.5,10],[2.5,10],[3.5,10],[4.5,10],[5.5,10],[6.5,10],[7.5,10],[8.5,10],[9.5,10],[10,9.5]],[[1.5,2.5],[0.5,1.5],[1.5,0.5],[2.5,1.5],[1.5,2.5]],[[3.5,5.5],[2.5,4.5],[3.5,3.5],[4.5,4.5],[3.5,5.5]],[[2.5,8.5],[1.5,7.5],[2.5,6.5],[3.5,7.5],[2.5,8.5]],[[7.5,8.5],[6.5,7.5],[7.5,6.5],[8.5,7.5],[7.5,8.5]]]]});
});

it("contours(values) returns the expected result for a +Infinity value", () => {
const c = contours().size([10, 10]).thresholds([0.5]);
assert.deepStrictEqual(c([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, +Infinity, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, +Infinity, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]), [
{
"type": "MultiPolygon",
"value": 0.5,
"coordinates": [
[
[[6, 7.5], [6, 6.5], [6, 5.5], [6, 4.5], [6, 3.5], [5.5, 3], [4.5, 3],
[3.5, 3], [3, 3.5], [3, 4.5], [3, 5.5], [3, 6.5], [3, 7.5], [3.5, 8],
[4.5, 8], [5.5, 8], [6, 7.5]]
]
]
}
]);
});

it("contour(values, invalid value) throws an error", () => {
for (const value of [NaN, null, undefined, "a string"]) {
assert.throws(() => contours().size([3, 3]).contour([1, 2, 3, 4, 5, 6, 7, 8, 9], value), /invalid value/);
}
});

0 comments on commit f81b2b6

Please sign in to comment.