diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..ad94bc7
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "npm.packageManager": "yarn"
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..bfd6d50
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,16 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "npm",
+ "script": "test",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": [],
+ "label": "yarn: test",
+ "detail": "tape 'test/**/*-test.js' && eslint src"
+ }
+ ]
+}
diff --git a/README.md b/README.md
index 5abb632..04bc6df 100644
--- a/README.md
+++ b/README.md
@@ -98,7 +98,62 @@ If *size* is specified, sets the expected size of the input *values* grid to the
# contours.smooth([smooth]) · [Source](https://github.com/d3/d3-contour/blob/master/src/contours.js), [Examples](https://observablehq.com/@d3/contours-smooth)
-If *smooth* is specified, sets whether or not the generated contour polygons are smoothed using linear interpolation. If *smooth* is not specified, returns the current smoothing flag, which defaults to true.
+If *smooth* is specified, sets the smoothing method to use when generating the contour polygons. If *smooth* is not specified, returns the current smoothing method, which defaults to true.
+
+The available *smooth* options are:
+
+- `false`: Smoothing is disabled.
+- `true` or `"linear"` (Default): Linear interpolation smoothing, as described in the original [Marching Squares algorithm](https://en.wikipedia.org/wiki/Marching_squares).
+- `"linearDual"`: Dual linear interpolation smoothing, as described in the [Dual Marching Squares algorithm](https://ieeexplore.ieee.org/document/7459173).
+
+In general, the quality of each smoothing method is inversely proportional to its runtime performance.
+
+Additionally, the density of the source data impacts contour smoothness: the differences between the smoothing methods are more noticeable with low-density data than they are with high-density data. With _very_ high-density data, there is no discernible quality difference between the three methods.
+
+
+
+
+ |
+ Low-density data |
+ High-density data |
+
+
+ Smoothing method |
+ Contour quality |
+ Performance cost* |
+ Contour quality |
+ Performance cost* |
+
+
+
+
+ None |
+ false |
+ Poor |
+ +0% |
+ Good |
+ +0% |
+
+
+ Linear (Default) |
+ true or
"linear" |
+ Good |
+ +2.40% |
+ Best |
+ +1.55% |
+
+
+ Dual linear |
+ "linearDual" |
+ Best |
+ +2.70% |
+ Best |
+ +2.00% |
+
+
+
+
+\* Estimated performance cost based on 2 sample datasets.
# contours.thresholds([thresholds]) · [Source](https://github.com/d3/d3-contour/blob/master/src/contours.js), [Examples](https://observablehq.com/@d3/volcano-contours)
diff --git a/src/contours.js b/src/contours.js
index 614e9ec..6b7b6fb 100644
--- a/src/contours.js
+++ b/src/contours.js
@@ -182,6 +182,39 @@ export default function() {
});
}
+ /**
+ * Applies smoothing to an iso-ring according to the Dual Marching Squares algorithm.
+ * @param {[number, number][]} ring The sorted list of (x,y) coordinates of points for a given iso-ring.
+ * @param {number[]} values The underlying grid values.
+ * @param {number} value The iso-value for this iso-ring.
+ */
+ function smoothLinearDual(ring, values, value) {
+ var point, x, y, x1, y1;
+
+ // The first step in Dual Marching Squares smoothing is linear interpolation.
+ smoothLinear(ring, values, value);
+
+ for (var i = 0; i < ring.length; i++) {
+ point = ring[i];
+ x = point[0];
+ y = point[1];
+
+ if (i < ring.length - 1) {
+ // Next point
+ x1 = ring[i + 1][0];
+ y1 = ring[i + 1][1];
+
+ // Set the current point to the midpoint between it and the next point.
+ point[0] = x + (x1 - x) / 2;
+ point[1] = y + (y1 - y) / 2;
+ } else {
+ // This is the last point, complete the ring by matching the first point
+ point[0] = ring[0][0];
+ point[1] = ring[0][1];
+ }
+ }
+ }
+
contours.contour = contour;
contours.size = function(_) {
@@ -196,7 +229,7 @@ export default function() {
};
contours.smooth = function(_) {
- return arguments.length ? (smooth = _ ? smoothLinear : noop, contours) : smooth === smoothLinear;
+ return arguments.length ? (smooth = _ === 'linearDual' ? smoothLinearDual : _ ? smoothLinear : noop, contours) : smooth === smoothLinear || smooth === smoothLinearDual;
};
return contours;
diff --git a/test/contours-test.js b/test/contours-test.js
index 650c5e7..8cc92fa 100644
--- a/test/contours-test.js
+++ b/test/contours-test.js
@@ -24,6 +24,31 @@ tape("contours(values) returns the expected result for an empty polygon", functi
test.end();
});
+tape("contours.smooth('linearDual')(values) returns the expected result for an empty polygon", function(test) {
+ var contours = d3.contours().smooth('linearDual').size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 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, 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, 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, 0
+ ]), [
+ {
+ "type": "MultiPolygon",
+ "value": 0.5,
+ "coordinates": []
+ }
+ ]);
+ test.end();
+});
+
+
+
tape("contours(values) returns the expected result for a simple polygon", function(test) {
var contours = d3.contours().size([10, 10]).thresholds([0.5]);
test.deepEqual(contours([
@@ -53,6 +78,105 @@ tape("contours(values) returns the expected result for a simple polygon", functi
test.end();
});
+tape("contours.smooth(false)(values) returns the expected result for a simple polygon", function(test) {
+ var contours = d3.contours().smooth(false).size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 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, 1, 1, 0, 0, 0, 0,
+ 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
+ 0, 0, 0, 1, 1, 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], // Right edge
+ [5.5, 3], [4.5, 3], [3.5, 3], // Top edge
+ [3, 3.5], [3, 4.5], [3, 5.5], [3, 6.5], [3, 7.5], // Left edge
+ [3.5, 8], [4.5, 8], [5.5, 8], // Bottom edge
+ [6, 7.5]
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
+tape("contours.smooth(true)(values) returns the expected result for a simple polygon", function(test) {
+ var contours = d3.contours().smooth(true).size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 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, 1, 1, 0, 0, 0, 0,
+ 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
+ 0, 0, 0, 1, 1, 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], // Right edge
+ [5.5, 3], [4.5, 3], [3.5, 3], // Top edge
+ [3, 3.5], [3, 4.5], [3, 5.5], [3, 6.5], [3, 7.5], // Left edge
+ [3.5, 8], [4.5, 8], [5.5, 8], // Bottom edge
+ [6, 7.5]
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
+tape("contours.smooth('linearDual')(values) returns the expected result for a simple polygon", function(test) {
+ var contours = d3.contours().smooth('linearDual').size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 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, 1, 1, 0, 0, 0, 0,
+ 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
+ 0, 0, 0, 1, 1, 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], [6, 6], [6, 5], [6, 4], [5.75, 3.25], // Right edge
+ [5, 3], [4, 3], [3.25, 3.25], // Top edge
+ [3, 4], [3, 5], [3, 6], [3, 7], [3.25, 7.75], // Left edge
+ [4, 8], [5, 8], [5.75, 7.75], // Bottom edge
+ [6, 7]
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
tape("contours(values).contour(value) returns the expected result for a simple polygon", function(test) {
var contours = d3.contours().size([10, 10]);
test.deepEqual(contours.contour([
@@ -80,7 +204,106 @@ tape("contours(values).contour(value) returns the expected result for a simple p
test.end();
});
-tape("contours.smooth(false)(values) returns the expected result for a simple polygon", function(test) {
+tape("contours(values) returns the expected result for a corner polygon", function(test) {
+ var contours = d3.contours().size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ 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, 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, 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
+ ]), [
+ {
+ "type": "MultiPolygon",
+ "value": 0.5,
+ "coordinates": [
+ [
+ [
+ [2, 1.5], [2, 0.5], // Right edge
+ [1.5, 0], [0.5, 0], // Top edge
+ [0, 0.5], [0, 1.5], // Left edge
+ [0.5, 2], [1.5, 2], // Bottom edge
+ [2, 1.5]
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
+tape("contours.smooth(false)(values) returns the expected result for a polygon in the corner", function(test) {
+ var contours = d3.contours().smooth(false).size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ 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, 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, 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
+ ]), [
+ {
+ "type": "MultiPolygon",
+ "value": 0.5,
+ "coordinates": [
+ [
+ [
+ [2, 1.5], [2, 0.5], // Right edge
+ [1.5, 0], [0.5, 0], // Top edge
+ [0, 0.5], [0, 1.5], // Left edge
+ [0.5, 2], [1.5, 2], // Bottom edge
+ [2, 1.5]
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
+tape("contours.smooth('linearDual')(values) returns the expected result for polygon in the corner", function(test) {
+ var contours = d3.contours().smooth('linearDual').size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ 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, 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, 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
+ ]), [
+ {
+ "type": "MultiPolygon",
+ "value": 0.5,
+ "coordinates": [
+ [
+ [
+ [2, 1], [1.75, 0.25], // Right edge
+ [1, 0], [0.25, 0.25], // Top edge
+ [0, 1], [0.25, 1.75], // Left edge
+ [1, 2], [1.75, 1.75], // Bottom edge
+ [2, 1]
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
+tape("contours.smooth(false)(values) returns the expected result for a complex polygon", function(test) {
var contours = d3.contours().smooth(false).size([10, 10]).thresholds([0.5]);
test.deepEqual(contours([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
@@ -109,6 +332,72 @@ tape("contours.smooth(false)(values) returns the expected result for a simple po
test.end();
});
+tape("contours.smooth(true)(values) returns the expected result for a complex polygon", function(test) {
+ var contours = d3.contours().smooth(true).size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 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, 2, 1, 2, 0, 0, 0, 0,
+ 0, 0, 0, 2, 2, 2, 0, 0, 0, 0,
+ 0, 0, 0, 1, 2, 1, 0, 0, 0, 0,
+ 0, 0, 0, 2, 2, 2, 0, 0, 0, 0,
+ 0, 0, 0, 2, 1, 2, 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.25, 7.5], [6.25, 6.5], [6, 5.5], [6.25, 4.5], [6.25, 3.5], // Right edge
+ [5.5, 2.75], [4.5, 3], [3.5, 2.75], // Top edge
+ [2.75, 3.5], [2.75, 4.5], [3, 5.5], [2.75, 6.5], [2.75, 7.5], // Left edge
+ [3.5, 8.25], [4.5, 8], [5.5, 8.25], // Bottom edge
+ [6.25, 7.5]
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
+tape("contours.smooth('linearDual')(values) returns the expected result for a complex polygon", function(test) {
+ var contours = d3.contours().smooth('linearDual').size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 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, 2, 1, 2, 0, 0, 0, 0,
+ 0, 0, 0, 2, 2, 2, 0, 0, 0, 0,
+ 0, 0, 0, 1, 2, 1, 0, 0, 0, 0,
+ 0, 0, 0, 2, 2, 2, 0, 0, 0, 0,
+ 0, 0, 0, 2, 1, 2, 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.25, 7], [6.125, 6], [6.125, 5], [6.25, 4], [5.875, 3.125], // Right edge
+ [5, 2.875], [4, 2.875], [3.125, 3.125], // Top edge
+ [2.75, 4], [2.875, 5], [2.875, 6], [2.75, 7], [3.125, 7.875], // Left edge
+ [4, 8.125], [5, 8.125], [5.875, 7.875], // Bottom edge
+ [6.25, 7]
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
tape("contours(values) returns the expected result for a polygon with a hole", function(test) {
var contours = d3.contours().size([10, 10]).thresholds([0.5]);
test.deepEqual(contours([
@@ -140,6 +429,90 @@ tape("contours(values) returns the expected result for a polygon with a hole", f
test.end();
});
+tape("contours.smooth(true)(values) returns the expected result for a polygon with a hole", function(test) {
+ var contours = d3.contours().smooth(true).size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 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, 0, 1, 0, 0, 0, 0,
+ 0, 0, 0, 1, 0, 1, 0, 0, 0, 0,
+ 0, 0, 0, 1, 0, 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": [
+ [
+ [
+ // Outer polygon, counter-clockwise path
+ [6, 7.5], [6, 6.5], [6, 5.5], [6, 4.5], [6, 3.5], // Right edge
+ [5.5, 3], [4.5, 3], [3.5, 3], // Top edge
+ [3, 3.5], [3, 4.5], [3, 5.5], [3, 6.5], [3, 7.5], // Left edge
+ [3.5, 8], [4.5, 8], [5.5, 8], // Bottom edge
+ [6, 7.5]
+ ],
+ [
+ // Inner polygon, clockwise path
+ [4.5, 7], // Bottom point
+ [4, 6.5], [4, 5.5], [4, 4.5], // Left edge
+ [4.5, 4], // Top point
+ [5, 4.5], [5, 5.5], [5, 6.5], // Right edge
+ [4.5, 7]
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
+tape("contours.smooth('linearDual')(values) returns the expected result for a polygon with a hole", function(test) {
+ var contours = d3.contours().smooth('linearDual').size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 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, 0, 1, 0, 0, 0, 0,
+ 0, 0, 0, 1, 0, 1, 0, 0, 0, 0,
+ 0, 0, 0, 1, 0, 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": [
+ [
+ [
+ // Outer polygon, counter-clockwise path
+ [6, 7], [6, 6], [6, 5], [6, 4], [5.75, 3.25], // Right edge
+ [5, 3], [4, 3], [3.25, 3.25], // Top edge
+ [3, 4], [3, 5], [3, 6], [3, 7], [3.25, 7.75], // Left edge
+ [4, 8], [5, 8], [5.75, 7.75], // Bottom edge
+ [6, 7]
+ ],
+ [
+ // Inner polygon, clockwise path
+ [4.25, 6.75], // Bottom edge
+ [4, 6], [4, 5], // Left edge
+ [4.25, 4.25], [4.75, 4.25], // Top edge
+ [5, 5], [5, 6], // Right edge
+ [4.75, 6.75], [4.25, 6.75] // Bottom edge
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
tape("contours(values) returns the expected result for a multipolygon", function(test) {
var contours = d3.contours().size([10, 10]).thresholds([0.5]);
test.deepEqual(contours([
@@ -207,6 +580,126 @@ tape("contours(values) returns the expected result for a multipolygon with holes
test.end();
});
+tape("contours.smooth(false)(values) returns the expected result for a multipolygon with holes", function(test) {
+ var contours = d3.contours().smooth(false).size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 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, 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, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ ]), [
+ {
+ "type": "MultiPolygon",
+ "value": 0.5,
+ "coordinates": [
+ [
+ [
+ // Outer left polygon, counter-clockwise path
+ [4, 5.5], [4, 4.5], [4, 3.5], // Right edge
+ [3.5, 3], [2.5, 3], [1.5, 3], // Top edge
+ [1, 3.5], [1, 4.5], [1, 5.5], // Left edge
+ [1.5, 6], [2.5, 6], [3.5, 6], // Bottom edge
+ [4,5.5]
+ ],
+ [
+ // Inner left polygon, clockwise path
+ [2.5, 5], // Bottom
+ [2, 4.5], // Left
+ [2.5, 4], // Top
+ [3, 4.5], // Right
+ [2.5, 5]
+ ]
+ ],
+ [
+ [
+ // Outer right polygon, counter-clockwise path
+ [8, 5.5], [8, 4.5], [8, 3.5], // Right edge
+ [7.5, 3], [6.5, 3], [5.5, 3], // Top edge
+ [5, 3.5], [5, 4.5], [5, 5.5], // Left edge
+ [5.5, 6], [6.5, 6], [7.5, 6], // Bottom edge
+ [8, 5.5]
+ ],
+ [
+ // Inner right polygon, clockwise path
+ [6.5, 5], // Bottom
+ [6, 4.5], // Left
+ [6.5, 4], // Top
+ [7, 4.5], // Right
+ [6.5, 5]
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
+tape("contours.smooth('linearDual')(values) returns the expected result for a multipolygon with holes", function(test) {
+ var contours = d3.contours().smooth('linearDual').size([10, 10]).thresholds([0.5]);
+ test.deepEqual(contours([
+ 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, 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, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ ]), [
+ {
+ "type": "MultiPolygon",
+ "value": 0.5,
+ "coordinates": [
+ [
+ [
+ // Outer left polygon, counter-clockwise path
+ [4, 5], [4, 4], // Right edge
+ [3.75, 3.25], [3, 3], [2, 3], [1.25, 3.25], // Top edge
+ [1, 4], [1, 5], // Left edge
+ [1.25, 5.75], [2, 6], [3, 6], [3.75, 5.75], // Bottom edge
+ [4, 5]
+ ],
+ [
+ // Inner left polygon, clockwise path
+ [2.25, 4.75], // Bottom-left
+ [2.25, 4.25], // Top-left
+ [2.75, 4.25], // Top-right
+ [2.75, 4.75], // Bottom-right
+ [2.25, 4.75]
+ ]
+ ],
+ [
+ [
+ // Outer right polygon, counter-clockwise path
+ [8, 5], [8, 4], // Right edge
+ [7.75, 3.25], [7, 3], [6, 3], [5.25, 3.25], // Top edge
+ [5, 4], [5, 5], // Left edge
+ [5.25, 5.75], [6, 6], [7, 6], [7.75, 5.75], // Bottom edge
+ [8, 5]
+ ],
+ [
+ // Inner right polygon, clockwise path
+ [6.25, 4.75], // Bottom-left
+ [6.25, 4.25], // Top-left
+ [6.75, 4.25], // Top-right
+ [6.75, 4.75], // Bottom-right
+ [6.25, 4.75]
+ ]
+ ]
+ ]
+ }
+ ]);
+ test.end();
+});
+
tape("contours.size(…) validates the specified size", function(test) {
test.deepEqual(d3.contours().size([1, 2]).size(), [1, 2]);
test.deepEqual(d3.contours().size([0, 0]).size(), [0, 0]);