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

null, undefined, NaN and -Infinity create holes #61

Merged
merged 9 commits into from
Jan 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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/);
}
});