Skip to content

Commit 11ae1ab

Browse files
committed
expand the matrix on the edges (with blurred values), then contract the contour back to the frame
closes #72
1 parent 9fc65d5 commit 11ae1ab

File tree

5 files changed

+155
-73
lines changed

5 files changed

+155
-73
lines changed

src/contours.js

+83-51
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import {extent, nice, thresholdSturges, ticks} from "d3-array";
1+
import {blur, extent, nice, thresholdSturges, ticks} from "d3-array";
22
import {slice} from "./array.js";
33
import ascending from "./ascending.js";
44
import area from "./area.js";
55
import constant from "./constant.js";
66
import contains from "./contains.js";
77
import noop from "./noop.js";
88

9-
var cases = [
9+
const cases = [
1010
[],
1111
[[[1.0, 1.5], [0.5, 1.0]]],
1212
[[[1.5, 1.0], [1.0, 1.5]]],
@@ -25,14 +25,20 @@ var cases = [
2525
[]
2626
];
2727

28+
const blurEdges = 0.5;
29+
30+
function clamp(x, lo, hi) {
31+
return x < lo ? lo : x > hi ? hi : x;
32+
}
33+
2834
export default function() {
29-
var dx = 1,
30-
dy = 1,
31-
threshold = thresholdSturges,
32-
smooth = smoothLinear;
35+
let dx = 1;
36+
let dy = 1;
37+
let threshold = thresholdSturges;
38+
let smooth = smoothLinear;
3339

3440
function contours(values) {
35-
var tz = threshold(values);
41+
let tz = threshold(values);
3642

3743
// Convert number of thresholds into uniform thresholds.
3844
if (!Array.isArray(tz)) {
@@ -53,17 +59,45 @@ export default function() {
5359
const v = value == null ? NaN : +value;
5460
if (isNaN(v)) throw new Error(`invalid value: ${value}`);
5561

56-
var polygons = [],
57-
holes = [];
62+
// Don’t round the corners by clamping values on the edge.
63+
const bottom = values.slice(0, dx);
64+
const top = values.slice(-dx);
65+
const left = Array.from({length: dy}, (_, i) => values[i * dx]);
66+
const right = Array.from({length: dy}, (_, i) => values[i * dx + dx - 1]);
67+
blur(bottom, blurEdges);
68+
blur(top, blurEdges);
69+
blur(left, blurEdges);
70+
blur(right, blurEdges);
71+
72+
function get(x, y) {
73+
const x0 = clamp(x, 0, dx - 1);
74+
const y0 = clamp(y, 0, dy - 1);
75+
if (y < 0) return bottom[x0];
76+
if (y >= dy) return top[x0];
77+
if (x < 0) return left[y0];
78+
if (x >= dx) return right[y0];
79+
return values[x0 + y0 * dx];
80+
}
5881

59-
isorings(values, v, function(ring) {
60-
smooth(ring, values, v);
61-
if (area(ring) > 0) polygons.push([ring]);
62-
else holes.push(ring);
82+
const polygons = [];
83+
const holes = [];
84+
85+
isorings(get, value, function(ring) {
86+
smooth(ring, get, value);
87+
const r = [];
88+
let x0, y0;
89+
for (const point of ring) {
90+
const x = clamp(point[0], 0, dx);
91+
const y = clamp(point[1], 0, dy);
92+
if (x !== x0 || y !== y0) r.push([(x0 = x), (y0 = y)]);
93+
}
94+
const a = area(r);
95+
if (a > 0) polygons.push([r]);
96+
else if (a < 0) holes.push(r);
6397
});
6498

6599
holes.forEach(function(hole) {
66-
for (var i = 0, n = polygons.length, polygon; i < n; ++i) {
100+
for (let i = 0, n = polygons.length, polygon; i < n; ++i) {
67101
if (contains((polygon = polygons[i])[0], hole) !== -1) {
68102
polygon.push(hole);
69103
return;
@@ -80,51 +114,52 @@ export default function() {
80114

81115
// Marching squares with isolines stitched into rings.
82116
// Based on https://github.com/topojson/topojson-client/blob/v3.0.0/src/stitch.js
83-
function isorings(values, value, callback) {
84-
var fragmentByStart = new Array,
85-
fragmentByEnd = new Array,
86-
x, y, t0, t1, t2, t3;
117+
function isorings(get, value, callback) {
118+
const test = (x, y) => above(get(x, y), value);
119+
const fragmentByStart = new Array;
120+
const fragmentByEnd = new Array;
121+
let x, y, t0, t1, t2, t3;
87122

88123
// Special case for the first row (y = -1, t2 = t3 = 0).
89-
x = y = -1;
90-
t1 = above(values[0], value);
124+
x = y = -2;
125+
t1 = test(-1, -1);
91126
cases[t1 << 1].forEach(stitch);
92-
while (++x < dx - 1) {
93-
t0 = t1, t1 = above(values[x + 1], value);
127+
while (++x < dx) {
128+
t0 = t1, t1 = test(x + 1, -1);
94129
cases[t0 | t1 << 1].forEach(stitch);
95130
}
96131
cases[t1 << 0].forEach(stitch);
97132

98133
// General case for the intermediate rows.
99-
while (++y < dy - 1) {
100-
x = -1;
101-
t1 = above(values[y * dx + dx], value);
102-
t2 = above(values[y * dx], value);
134+
while (++y < dy) {
135+
x = -2;
136+
t1 = test(x + 1, y + 1);
137+
t2 = test(x + 1, y);
103138
cases[t1 << 1 | t2 << 2].forEach(stitch);
104-
while (++x < dx - 1) {
105-
t0 = t1, t1 = above(values[y * dx + dx + x + 1], value);
106-
t3 = t2, t2 = above(values[y * dx + x + 1], value);
139+
while (++x < dx) {
140+
t0 = t1, t1 = test(x + 1, y + 1);
141+
t3 = t2, t2 = test(x + 1, y);
107142
cases[t0 | t1 << 1 | t2 << 2 | t3 << 3].forEach(stitch);
108143
}
109144
cases[t1 | t2 << 3].forEach(stitch);
110145
}
111146

112147
// Special case for the last row (y = dy - 1, t0 = t1 = 0).
113-
x = -1;
114-
t2 = values[y * dx] >= value;
148+
x = -2;
149+
t2 = test(x, y);
115150
cases[t2 << 2].forEach(stitch);
116-
while (++x < dx - 1) {
117-
t3 = t2, t2 = above(values[y * dx + x + 1], value);
151+
while (++x < dx) {
152+
t3 = t2, t2 = test(x + 1, y);
118153
cases[t2 << 2 | t3 << 3].forEach(stitch);
119154
}
120155
cases[t2 << 3].forEach(stitch);
121156

122157
function stitch(line) {
123-
var start = [line[0][0] + x, line[0][1] + y],
124-
end = [line[1][0] + x, line[1][1] + y],
125-
startIndex = index(start),
126-
endIndex = index(end),
127-
f, g;
158+
const start = [line[0][0] + x, line[0][1] + y];
159+
const end = [line[1][0] + x, line[1][1] + y];
160+
const startIndex = index(start);
161+
const endIndex = index(end);
162+
let f, g;
128163
if (f = fragmentByEnd[startIndex]) {
129164
if (g = fragmentByStart[endIndex]) {
130165
delete fragmentByEnd[f.end];
@@ -165,27 +200,24 @@ export default function() {
165200
return point[0] * 2 + point[1] * (dx + 1) * 4;
166201
}
167202

168-
function smoothLinear(ring, values, value) {
203+
function smoothLinear(ring, get, value) {
169204
ring.forEach(function(point) {
170-
var x = point[0],
171-
y = point[1],
172-
xt = x | 0,
173-
yt = y | 0,
174-
v1 = valid(values[yt * dx + xt]);
175-
if (x > 0 && x < dx && xt === x) {
176-
point[0] = smooth1(x, valid(values[yt * dx + xt - 1]), v1, value);
177-
}
178-
if (y > 0 && y < dy && yt === y) {
179-
point[1] = smooth1(y, valid(values[(yt - 1) * dx + xt]), v1, value);
180-
}
205+
const x = point[0];
206+
const y = point[1];
207+
const xt = x | 0;
208+
const yt = y | 0;
209+
const v1 = valid(get(xt, yt));
210+
if (x > 0 && x < dx && xt === x) point[0] = smooth1(x, valid(get(xt - 1, yt)), v1, value);
211+
if (y > 0 && y < dy && yt === y) point[1] = smooth1(y, valid(get(xt, yt - 1)), v1, value);
181212
});
182213
}
183214

184215
contours.contour = contour;
185216

186217
contours.size = function(_) {
187218
if (!arguments.length) return [dx, dy];
188-
var _0 = Math.floor(_[0]), _1 = Math.floor(_[1]);
219+
const _0 = Math.floor(_[0]);
220+
const _1 = Math.floor(_[1]);
189221
if (!(_0 >= 0 && _1 >= 0)) throw new Error("invalid size");
190222
return dx = _0, dy = _1, contours;
191223
};

test/contours-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ it("contours(values) treats null, undefined, NaN and -Infinity as holes", () =>
252252
1, 1, NaN, 1, 1, 1, 2, -Infinity, 2, 1,
253253
1, 1, 1, 1, 1, 1, 2, 2, 2, 1,
254254
1, 1, 1, 1, 1, 1, 1, 1, 1, 1
255-
], 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]]]]});
255+
], 0), {"type":"MultiPolygon","value":0,"coordinates":[[[[10,10],[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],[10,0],[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],[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,10],[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,10]],[[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]]]]});
256256
});
257257

258258
it("contours(values) returns the expected result for a +Infinity value", () => {

0 commit comments

Comments
 (0)