contours = {
const { ascending, thresholdSturges, extent, ticks, nice } = d3;
const slice = Array.prototype.slice;
const noop = () => {};
return function () {
var dx = 1,
dy = 1,
threshold = thresholdSturges,
smooth = smoothLinear;
function contours(values) {
mutable debug = [];
mutable inserts = [];
var tz = threshold(values);
if (!Array.isArray(tz)) {
const e = extent(values, finite);
tz = ticks(...nice(e[0], e[1], tz), tz);
while (tz[tz.length - 1] >= e[1]) tz.pop();
while (tz[1] < e[0]) tz.shift();
} else {
tz = tz.slice().sort(ascending);
}
return tz.map((value) => contour(values, value));
}
// if (xa === 0 && xa !== xb) insert = [0, yb];
// if (xb === dx && xa !== xb) insert = [dx, ya];
// if (xa === dx && xa !== xb) insert = [dx, yb];
// if (yb === 0 && ya !== yb) insert = [xa, 0];
// if (ya === 0 && ya !== yb) insert = [xb, 0];
// if (yb === dy && ya !== yb) insert = [xa, dy];
// if (ya === dy && ya !== yb) insert = [xb, dy];
// // if (xa===0 && yb === dy) {insert=null}
// (xa = xb), (ya = yb);
// if (insert) {
// mutable inserts.push(insert);
// ring.splice(i++, 0, insert);
// }
// }
//}
// fix corners only
function corners(ring) {
let [xa, ya] = ring[ring.length - 1];
for (let i = 0; i < ring.length; ++i) {
const [xb, yb] = ring[i];
let insert;
if (xa < 0.5 && yb > dy - 0.5) insert = [xa, yb];
if (xb > dx - 0.5 && ya > dy - 0.5) insert = [xb, ya];
if (xa > dx - 0.5 && yb < 0.5) insert = [xa, yb];
if (xb < 0.5 && ya < 0.5) insert = [xb, ya];
(xa = xb), (ya = yb);
if (insert) {
mutable inserts.push(insert);
ring.splice(i++, 0, insert);
}
}
}
// 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, v, function (ring) {
if (fixEdges) corners(ring);
mutable debug.push(ring);
if (area(ring) > 0) polygons.push([ring]);
else holes.push(ring);
smooth(ring, values, v);
});
holes.forEach(function (hole) {
for (var i = 0, n = polygons.length, polygon; i < n; ++i) {
if (contains((polygon = polygons[i])[0], hole) !== -1) {
polygon.push(hole);
return;
}
}
});
return {
type: "MultiPolygon",
value: value,
coordinates: polygons
};
}
// Marching squares with isolines stitched into rings.
// Based on https://github.com/topojson/topojson-client/blob/v3.0.0/src/stitch.js
function isorings(values, value, callback) {
var fragmentByStart = new Array(),
fragmentByEnd = new Array(),
x,
y,
t0,
t1,
t2,
t3;
// Special case for the first row (y = -1, t2 = t3 = 0).
x = y = -1;
t1 = above(values[0], value);
mutable debug.push(t1 << 1);
cases[t1 << 1].forEach(stitch);
while (++x < dx - 1) {
(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 = 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 = 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);
}
// Special case for the last row (y = dy - 1, t0 = t1 = 0).
x = -1;
t2 = above(values[y * dx], value);
const eq = t2 && above(value, values[y * dx]);
cases[t2 << 2].forEach(stitch);
while (++x < dx - 1) {
(t3 = t2), (t2 = above(values[y * dx + x + 1], value));
cases[(t2 << 2) | (t3 << 3)].forEach(stitch);
}
cases[t2 << 3].forEach(stitch);
function stitch(line) {
var start = [line[0][0] + x, line[0][1] + y],
end = [line[1][0] + x, line[1][1] + y],
startIndex = index(start),
endIndex = index(end),
f,
g;
if ((f = fragmentByEnd[startIndex])) {
if ((g = fragmentByStart[endIndex])) {
delete fragmentByEnd[f.end];
delete fragmentByStart[g.start];
if (f === g) {
f.ring.push(end);
callback(f.ring);
} else {
fragmentByStart[f.start] = fragmentByEnd[g.end] = {
start: f.start,
end: g.end,
ring: f.ring.concat(g.ring)
};
}
} else {
delete fragmentByEnd[f.end];
f.ring.push(end);
fragmentByEnd[(f.end = endIndex)] = f;
}
} else if ((f = fragmentByStart[endIndex])) {
if ((g = fragmentByEnd[startIndex])) {
delete fragmentByStart[f.start];
delete fragmentByEnd[g.end];
if (f === g) {
f.ring.push(end);
callback(f.ring);
} else {
fragmentByStart[g.start] = fragmentByEnd[f.end] = {
start: g.start,
end: f.end,
ring: g.ring.concat(f.ring)
};
}
} else {
delete fragmentByStart[f.start];
f.ring.unshift(start);
fragmentByStart[(f.start = startIndex)] = f;
}
} else {
fragmentByStart[startIndex] = fragmentByEnd[endIndex] = {
start: startIndex,
end: endIndex,
ring: [start, end]
};
}
}
}
function index(point) {
return point[0] * 2 + point[1] * (dx + 1) * 4;
}
function smoothLinear(ring, values, value) {
ring.forEach(function (point, i) {
var x = point[0],
y = point[1],
xt = x | 0,
yt = y | 0,
k = yt * dx + xt,
v0 = valid(values[k]);
// Special case for the first column: move horizontally
// TODO: do it for the last column, first row, last row…
if (x === 0) {
// estimate the value at the point, by linear interpolation (todo: bilinear model? in particular in the corners?)
const estimate = y === dy ? 2 * values[k - dx] - values[k - dx + 1] : 2 * v0 - values[k + 1];
point[0] += Math.max(0, smooth1(valid(estimate), y === dy ? valid(values[k - dx]) : v0, value));
}
if (x > 0 && x < dx && xt === x) {
if (yt === dy) {
point[0] += smooth1(
valid(values[k - dx - 1]),
valid(values[k - dx]),
value
);
} else {
point[0] += smooth1(valid(values[k - 1]), v0, value);
}
}
if (y > 0 && y < dy && yt === y) {
if (xt === dx) {
point[1] += smooth1(
valid(values[k - dx - 1]),
valid(values[k - 1]),
value
);
} else {
point[1] += smooth1(valid(values[(yt - 1) * dx + xt]), v0, value);
}
}
});
}
function clamp(x, min, max) {
return x < min ? min : x > max ? max : x;
}
contours.contour = contour;
contours.size = function (_) {
if (!arguments.length) return [dx, dy];
var _0 = Math.floor(_[0]),
_1 = Math.floor(_[1]);
if (!(_0 >= 0 && _1 >= 0)) throw new Error("invalid size");
return (dx = _0), (dy = _1), contours;
};
contours.thresholds = function (_) {
return arguments.length
? ((threshold =
typeof _ === "function"
? _
: Array.isArray(_)
? constant(slice.call(_))
: constant(_)),
contours)
: threshold;
};
contours.smooth = function (_) {
return arguments.length
? ((smooth = _ ? smoothLinear : noop), contours)
: smooth === smoothLinear;
};
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(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 isFinite(d) ? d - 0.5 : 0;
}
}