| 1 | import {extent, nice, thresholdSturges, ticks} from "d3-array";
|
|---|
| 2 | import {slice} from "./array.js";
|
|---|
| 3 | import ascending from "./ascending.js";
|
|---|
| 4 | import area from "./area.js";
|
|---|
| 5 | import constant from "./constant.js";
|
|---|
| 6 | import contains from "./contains.js";
|
|---|
| 7 | import noop from "./noop.js";
|
|---|
| 8 |
|
|---|
| 9 | var cases = [
|
|---|
| 10 | [],
|
|---|
| 11 | [[[1.0, 1.5], [0.5, 1.0]]],
|
|---|
| 12 | [[[1.5, 1.0], [1.0, 1.5]]],
|
|---|
| 13 | [[[1.5, 1.0], [0.5, 1.0]]],
|
|---|
| 14 | [[[1.0, 0.5], [1.5, 1.0]]],
|
|---|
| 15 | [[[1.0, 1.5], [0.5, 1.0]], [[1.0, 0.5], [1.5, 1.0]]],
|
|---|
| 16 | [[[1.0, 0.5], [1.0, 1.5]]],
|
|---|
| 17 | [[[1.0, 0.5], [0.5, 1.0]]],
|
|---|
| 18 | [[[0.5, 1.0], [1.0, 0.5]]],
|
|---|
| 19 | [[[1.0, 1.5], [1.0, 0.5]]],
|
|---|
| 20 | [[[0.5, 1.0], [1.0, 0.5]], [[1.5, 1.0], [1.0, 1.5]]],
|
|---|
| 21 | [[[1.5, 1.0], [1.0, 0.5]]],
|
|---|
| 22 | [[[0.5, 1.0], [1.5, 1.0]]],
|
|---|
| 23 | [[[1.0, 1.5], [1.5, 1.0]]],
|
|---|
| 24 | [[[0.5, 1.0], [1.0, 1.5]]],
|
|---|
| 25 | []
|
|---|
| 26 | ];
|
|---|
| 27 |
|
|---|
| 28 | export default function() {
|
|---|
| 29 | var dx = 1,
|
|---|
| 30 | dy = 1,
|
|---|
| 31 | threshold = thresholdSturges,
|
|---|
| 32 | smooth = smoothLinear;
|
|---|
| 33 |
|
|---|
| 34 | function contours(values) {
|
|---|
| 35 | var tz = threshold(values);
|
|---|
| 36 |
|
|---|
| 37 | // Convert number of thresholds into uniform thresholds.
|
|---|
| 38 | if (!Array.isArray(tz)) {
|
|---|
| 39 | const e = extent(values, finite);
|
|---|
| 40 | tz = ticks(...nice(e[0], e[1], tz), tz);
|
|---|
| 41 | while (tz[tz.length - 1] >= e[1]) tz.pop();
|
|---|
| 42 | while (tz[1] < e[0]) tz.shift();
|
|---|
| 43 | } else {
|
|---|
| 44 | tz = tz.slice().sort(ascending);
|
|---|
| 45 | }
|
|---|
| 46 |
|
|---|
| 47 | return tz.map(value => contour(values, value));
|
|---|
| 48 | }
|
|---|
| 49 |
|
|---|
| 50 | // Accumulate, smooth contour rings, assign holes to exterior rings.
|
|---|
| 51 | // Based on https://github.com/mbostock/shapefile/blob/v0.6.2/shp/polygon.js
|
|---|
| 52 | function contour(values, value) {
|
|---|
| 53 | const v = value == null ? NaN : +value;
|
|---|
| 54 | if (isNaN(v)) throw new Error(`invalid value: ${value}`);
|
|---|
| 55 |
|
|---|
| 56 | var polygons = [],
|
|---|
| 57 | holes = [];
|
|---|
| 58 |
|
|---|
| 59 | isorings(values, v, function(ring) {
|
|---|
| 60 | smooth(ring, values, v);
|
|---|
| 61 | if (area(ring) > 0) polygons.push([ring]);
|
|---|
| 62 | else holes.push(ring);
|
|---|
| 63 | });
|
|---|
| 64 |
|
|---|
| 65 | holes.forEach(function(hole) {
|
|---|
| 66 | for (var i = 0, n = polygons.length, polygon; i < n; ++i) {
|
|---|
| 67 | if (contains((polygon = polygons[i])[0], hole) !== -1) {
|
|---|
| 68 | polygon.push(hole);
|
|---|
| 69 | return;
|
|---|
| 70 | }
|
|---|
| 71 | }
|
|---|
| 72 | });
|
|---|
| 73 |
|
|---|
| 74 | return {
|
|---|
| 75 | type: "MultiPolygon",
|
|---|
| 76 | value: value,
|
|---|
| 77 | coordinates: polygons
|
|---|
| 78 | };
|
|---|
| 79 | }
|
|---|
| 80 |
|
|---|
| 81 | // Marching squares with isolines stitched into rings.
|
|---|
| 82 | // 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;
|
|---|
| 87 |
|
|---|
| 88 | // Special case for the first row (y = -1, t2 = t3 = 0).
|
|---|
| 89 | x = y = -1;
|
|---|
| 90 | t1 = above(values[0], value);
|
|---|
| 91 | cases[t1 << 1].forEach(stitch);
|
|---|
| 92 | while (++x < dx - 1) {
|
|---|
| 93 | t0 = t1, t1 = above(values[x + 1], value);
|
|---|
| 94 | cases[t0 | t1 << 1].forEach(stitch);
|
|---|
| 95 | }
|
|---|
| 96 | cases[t1 << 0].forEach(stitch);
|
|---|
| 97 |
|
|---|
| 98 | // 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);
|
|---|
| 103 | 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);
|
|---|
| 107 | cases[t0 | t1 << 1 | t2 << 2 | t3 << 3].forEach(stitch);
|
|---|
| 108 | }
|
|---|
| 109 | cases[t1 | t2 << 3].forEach(stitch);
|
|---|
| 110 | }
|
|---|
| 111 |
|
|---|
| 112 | // Special case for the last row (y = dy - 1, t0 = t1 = 0).
|
|---|
| 113 | x = -1;
|
|---|
| 114 | t2 = values[y * dx] >= value;
|
|---|
| 115 | cases[t2 << 2].forEach(stitch);
|
|---|
| 116 | while (++x < dx - 1) {
|
|---|
| 117 | t3 = t2, t2 = above(values[y * dx + x + 1], value);
|
|---|
| 118 | cases[t2 << 2 | t3 << 3].forEach(stitch);
|
|---|
| 119 | }
|
|---|
| 120 | cases[t2 << 3].forEach(stitch);
|
|---|
| 121 |
|
|---|
| 122 | 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;
|
|---|
| 128 | if (f = fragmentByEnd[startIndex]) {
|
|---|
| 129 | if (g = fragmentByStart[endIndex]) {
|
|---|
| 130 | delete fragmentByEnd[f.end];
|
|---|
| 131 | delete fragmentByStart[g.start];
|
|---|
| 132 | if (f === g) {
|
|---|
| 133 | f.ring.push(end);
|
|---|
| 134 | callback(f.ring);
|
|---|
| 135 | } else {
|
|---|
| 136 | fragmentByStart[f.start] = fragmentByEnd[g.end] = {start: f.start, end: g.end, ring: f.ring.concat(g.ring)};
|
|---|
| 137 | }
|
|---|
| 138 | } else {
|
|---|
| 139 | delete fragmentByEnd[f.end];
|
|---|
| 140 | f.ring.push(end);
|
|---|
| 141 | fragmentByEnd[f.end = endIndex] = f;
|
|---|
| 142 | }
|
|---|
| 143 | } else if (f = fragmentByStart[endIndex]) {
|
|---|
| 144 | if (g = fragmentByEnd[startIndex]) {
|
|---|
| 145 | delete fragmentByStart[f.start];
|
|---|
| 146 | delete fragmentByEnd[g.end];
|
|---|
| 147 | if (f === g) {
|
|---|
| 148 | f.ring.push(end);
|
|---|
| 149 | callback(f.ring);
|
|---|
| 150 | } else {
|
|---|
| 151 | fragmentByStart[g.start] = fragmentByEnd[f.end] = {start: g.start, end: f.end, ring: g.ring.concat(f.ring)};
|
|---|
| 152 | }
|
|---|
| 153 | } else {
|
|---|
| 154 | delete fragmentByStart[f.start];
|
|---|
| 155 | f.ring.unshift(start);
|
|---|
| 156 | fragmentByStart[f.start = startIndex] = f;
|
|---|
| 157 | }
|
|---|
| 158 | } else {
|
|---|
| 159 | fragmentByStart[startIndex] = fragmentByEnd[endIndex] = {start: startIndex, end: endIndex, ring: [start, end]};
|
|---|
| 160 | }
|
|---|
| 161 | }
|
|---|
| 162 | }
|
|---|
| 163 |
|
|---|
| 164 | function index(point) {
|
|---|
| 165 | return point[0] * 2 + point[1] * (dx + 1) * 4;
|
|---|
| 166 | }
|
|---|
| 167 |
|
|---|
| 168 | function smoothLinear(ring, values, value) {
|
|---|
| 169 | 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 | }
|
|---|
| 181 | });
|
|---|
| 182 | }
|
|---|
| 183 |
|
|---|
| 184 | contours.contour = contour;
|
|---|
| 185 |
|
|---|
| 186 | contours.size = function(_) {
|
|---|
| 187 | if (!arguments.length) return [dx, dy];
|
|---|
| 188 | var _0 = Math.floor(_[0]), _1 = Math.floor(_[1]);
|
|---|
| 189 | if (!(_0 >= 0 && _1 >= 0)) throw new Error("invalid size");
|
|---|
| 190 | return dx = _0, dy = _1, contours;
|
|---|
| 191 | };
|
|---|
| 192 |
|
|---|
| 193 | contours.thresholds = function(_) {
|
|---|
| 194 | return arguments.length ? (threshold = typeof _ === "function" ? _ : Array.isArray(_) ? constant(slice.call(_)) : constant(_), contours) : threshold;
|
|---|
| 195 | };
|
|---|
| 196 |
|
|---|
| 197 | contours.smooth = function(_) {
|
|---|
| 198 | return arguments.length ? (smooth = _ ? smoothLinear : noop, contours) : smooth === smoothLinear;
|
|---|
| 199 | };
|
|---|
| 200 |
|
|---|
| 201 | return contours;
|
|---|
| 202 | }
|
|---|
| 203 |
|
|---|
| 204 | // When computing the extent, ignore infinite values (as well as invalid ones).
|
|---|
| 205 | function finite(x) {
|
|---|
| 206 | return isFinite(x) ? x : NaN;
|
|---|
| 207 | }
|
|---|
| 208 |
|
|---|
| 209 | // Is the (possibly invalid) x greater than or equal to the (known valid) value?
|
|---|
| 210 | // Treat any invalid value as below negative infinity.
|
|---|
| 211 | function above(x, value) {
|
|---|
| 212 | return x == null ? false : +x >= value;
|
|---|
| 213 | }
|
|---|
| 214 |
|
|---|
| 215 | // During smoothing, treat any invalid value as negative infinity.
|
|---|
| 216 | function valid(v) {
|
|---|
| 217 | return v == null || isNaN(v = +v) ? -Infinity : v;
|
|---|
| 218 | }
|
|---|
| 219 |
|
|---|
| 220 | function smooth1(x, v0, v1, value) {
|
|---|
| 221 | const a = value - v0;
|
|---|
| 222 | const b = v1 - v0;
|
|---|
| 223 | const d = isFinite(a) || isFinite(b) ? a / b : Math.sign(a) / Math.sign(b);
|
|---|
| 224 | return isNaN(d) ? x : x + d - 0.5;
|
|---|
| 225 | }
|
|---|