Repository: d3/d3-contour Branch: main Commit: 8a3b95a65a46 Files: 25 Total size: 41.7 KB Directory structure: gitextract_9aal4xuq/ ├── .eslintrc.json ├── .github/ │ ├── eslint.json │ └── workflows/ │ └── node.js.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── rollup.config.js ├── src/ │ ├── area.js │ ├── array.js │ ├── ascending.js │ ├── constant.js │ ├── contains.js │ ├── contours.js │ ├── density.js │ ├── index.js │ └── noop.js └── test/ ├── .eslintrc.json ├── asserts.js ├── contours-test.js ├── data/ │ └── faithful.tsv ├── density-test.js ├── jsdom.js ├── snapshot.js └── snapshots/ └── index.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "eslint:recommended", "parserOptions": { "sourceType": "module", "ecmaVersion": 2020 }, "env": { "es6": true, "node": true, "browser": true }, "rules": { "no-cond-assign": 0, "no-constant-condition": 0, "no-sparse-arrays": 0, "no-unexpected-multiline": 0, "comma-dangle": ["error", "never"], "semi": [2, "always"] } } ================================================ FILE: .github/eslint.json ================================================ { "problemMatcher": [ { "owner": "eslint-compact", "pattern": [ { "regexp": "^(.+):\\sline\\s(\\d+),\\scol\\s(\\d+),\\s(Error|Warning|Info)\\s-\\s(.+)\\s\\((.+)\\)$", "file": 1, "line": 2, "column": 3, "severity": 4, "message": 5, "code": 6 } ] } ] } ================================================ FILE: .github/workflows/node.js.yml ================================================ # https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [14.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: yarn --frozen-lockfile - run: | echo ::add-matcher::.github/eslint.json yarn run eslint src test --format=compact - run: yarn test ================================================ FILE: .gitignore ================================================ *.sublime-workspace .DS_Store dist/ node_modules npm-debug.log ================================================ FILE: LICENSE ================================================ Copyright 2012-2023 Mike Bostock Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: README.md ================================================ # d3-contour This module computes contour polygons by applying [marching squares](https://en.wikipedia.org/wiki/Marching_squares) to a rectangular array of numeric values. ## Resources - [Documentation](https://d3js.org/d3-contour) - [Examples](https://observablehq.com/collection/@d3/d3-contour) - [Releases](https://github.com/d3/d3-contour/releases) - [Getting help](https://d3js.org/community) ================================================ FILE: package.json ================================================ { "name": "d3-contour", "version": "4.0.2", "description": "Compute contour polygons using marching squares.", "homepage": "https://d3js.org/d3-contour/", "repository": { "type": "git", "url": "https://github.com/d3/d3-contour.git" }, "keywords": [ "d3", "d3-module", "contour", "isoline" ], "license": "ISC", "author": { "name": "Mike Bostock", "url": "http://bost.ocks.org/mike" }, "type": "module", "files": [ "dist/**/*.js", "src/**/*.js" ], "module": "src/index.js", "main": "src/index.js", "jsdelivr": "dist/d3-contour.min.js", "unpkg": "dist/d3-contour.min.js", "exports": { "umd": "./dist/d3-contour.min.js", "default": "./src/index.js" }, "_moduleAliases": { "d3-contour": "./src/index.js" }, "sideEffects": false, "dependencies": { "d3-array": "^3.2.0" }, "devDependencies": { "d3-axis": "3", "d3-dsv": "3", "d3-fetch": "3", "d3-geo": "3", "d3-polygon": "3", "d3-scale": "4", "d3-selection": "3", "eslint": "8", "htl": "^0.3.1", "js-beautify": "1", "jsdom": "20", "mocha": "10", "module-alias": "2", "rollup": "3", "rollup-plugin-terser": "7" }, "scripts": { "test": "mkdir -p test/output && mocha -r module-alias/register 'test/**/*-test.js' test/snapshot.js && eslint src test", "prepublishOnly": "rm -rf dist && rollup -c", "postpublish": "git push && git push --tags && cd ../d3.github.com && git pull && cp ../${npm_package_name}/dist/${npm_package_name}.js ${npm_package_name}.v${npm_package_version%%.*}.js && cp ../${npm_package_name}/dist/${npm_package_name}.min.js ${npm_package_name}.v${npm_package_version%%.*}.min.js && git add ${npm_package_name}.v${npm_package_version%%.*}.js ${npm_package_name}.v${npm_package_version%%.*}.min.js && git commit -m \"${npm_package_name} ${npm_package_version}\" && git push && cd -" }, "engines": { "node": ">=12" } } ================================================ FILE: rollup.config.js ================================================ import {readFileSync} from "fs"; import {terser} from "rollup-plugin-terser"; import meta from "./package.json" assert {type: "json"}; // Extract copyrights from the LICENSE. const copyright = readFileSync("./LICENSE", "utf-8") .split(/\n/g) .filter(line => /^Copyright\s+/.test(line)) .map(line => line.replace(/^Copyright\s+/, "")) .join(", "); const config = { input: "src/index.js", external: Object.keys(meta.dependencies || {}).filter(key => /^d3-/.test(key)), output: { file: `dist/${meta.name}.js`, name: "d3", format: "umd", indent: false, extend: true, banner: `// ${meta.homepage} v${meta.version} Copyright ${copyright}`, globals: Object.assign({}, ...Object.keys(meta.dependencies || {}).filter(key => /^d3-/.test(key)).map(key => ({[key]: "d3"}))) }, plugins: [] }; export default [ config, { ...config, output: { ...config.output, file: `dist/${meta.name}.min.js` }, plugins: [ ...config.plugins, terser({ output: { preamble: config.output.banner } }) ] } ]; ================================================ FILE: src/area.js ================================================ export default function(ring) { var i = 0, n = ring.length, area = ring[n - 1][1] * ring[0][0] - ring[n - 1][0] * ring[0][1]; while (++i < n) area += ring[i - 1][1] * ring[i][0] - ring[i - 1][0] * ring[i][1]; return area; } ================================================ FILE: src/array.js ================================================ var array = Array.prototype; export var slice = array.slice; ================================================ FILE: src/ascending.js ================================================ export default function(a, b) { return a - b; } ================================================ FILE: src/constant.js ================================================ export default x => () => x; ================================================ FILE: src/contains.js ================================================ export default function(ring, hole) { var i = -1, n = hole.length, c; while (++i < n) if (c = ringContains(ring, hole[i])) return c; return 0; } function ringContains(ring, point) { var x = point[0], y = point[1], contains = -1; for (var i = 0, n = ring.length, j = n - 1; i < n; j = i++) { var pi = ring[i], xi = pi[0], yi = pi[1], pj = ring[j], xj = pj[0], yj = pj[1]; if (segmentContains(pi, pj, point)) return 0; if (((yi > y) !== (yj > y)) && ((x < (xj - xi) * (y - yi) / (yj - yi) + xi))) contains = -contains; } return contains; } function segmentContains(a, b, c) { var i; return collinear(a, b, c) && within(a[i = +(a[0] === b[0])], c[i], b[i]); } function collinear(a, b, c) { return (b[0] - a[0]) * (c[1] - a[1]) === (c[0] - a[0]) * (b[1] - a[1]); } function within(p, q, r) { return p <= q && q <= r || r <= q && q <= p; } ================================================ FILE: src/contours.js ================================================ import {extent, nice, thresholdSturges, ticks} from "d3-array"; import {slice} from "./array.js"; import ascending from "./ascending.js"; import area from "./area.js"; import constant from "./constant.js"; import contains from "./contains.js"; import noop from "./noop.js"; var cases = [ [], [[[1.0, 1.5], [0.5, 1.0]]], [[[1.5, 1.0], [1.0, 1.5]]], [[[1.5, 1.0], [0.5, 1.0]]], [[[1.0, 0.5], [1.5, 1.0]]], [[[1.0, 1.5], [0.5, 1.0]], [[1.0, 0.5], [1.5, 1.0]]], [[[1.0, 0.5], [1.0, 1.5]]], [[[1.0, 0.5], [0.5, 1.0]]], [[[0.5, 1.0], [1.0, 0.5]]], [[[1.0, 1.5], [1.0, 0.5]]], [[[0.5, 1.0], [1.0, 0.5]], [[1.5, 1.0], [1.0, 1.5]]], [[[1.5, 1.0], [1.0, 0.5]]], [[[0.5, 1.0], [1.5, 1.0]]], [[[1.0, 1.5], [1.5, 1.0]]], [[[0.5, 1.0], [1.0, 1.5]]], [] ]; export default function() { var dx = 1, dy = 1, threshold = thresholdSturges, smooth = smoothLinear; function contours(values) { var tz = threshold(values); // Convert number of thresholds into uniform thresholds. 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)); } // 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) { smooth(ring, values, v); if (area(ring) > 0) polygons.push([ring]); else holes.push(ring); }); 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); 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 = values[y * dx] >= value; 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) { var x = point[0], y = point[1], xt = x | 0, yt = y | 0, v1 = valid(values[yt * dx + xt]); if (x > 0 && x < dx && xt === x) { point[0] = smooth1(x, valid(values[yt * dx + xt - 1]), v1, value); } if (y > 0 && y < dy && yt === y) { point[1] = smooth1(y, valid(values[(yt - 1) * dx + xt]), v1, value); } }); } 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(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; } ================================================ FILE: src/density.js ================================================ import {blur2, max, ticks} from "d3-array"; import {slice} from "./array.js"; import constant from "./constant.js"; import Contours from "./contours.js"; function defaultX(d) { return d[0]; } function defaultY(d) { return d[1]; } function defaultWeight() { return 1; } export default function() { var x = defaultX, y = defaultY, weight = defaultWeight, dx = 960, dy = 500, r = 20, // blur radius k = 2, // log2(grid cell size) o = r * 3, // grid offset, to pad for blur n = (dx + o * 2) >> k, // grid width m = (dy + o * 2) >> k, // grid height threshold = constant(20); function grid(data) { var values = new Float32Array(n * m), pow2k = Math.pow(2, -k), i = -1; for (const d of data) { var xi = (x(d, ++i, data) + o) * pow2k, yi = (y(d, i, data) + o) * pow2k, wi = +weight(d, i, data); if (wi && xi >= 0 && xi < n && yi >= 0 && yi < m) { var x0 = Math.floor(xi), y0 = Math.floor(yi), xt = xi - x0 - 0.5, yt = yi - y0 - 0.5; values[x0 + y0 * n] += (1 - xt) * (1 - yt) * wi; values[x0 + 1 + y0 * n] += xt * (1 - yt) * wi; values[x0 + 1 + (y0 + 1) * n] += xt * yt * wi; values[x0 + (y0 + 1) * n] += (1 - xt) * yt * wi; } } blur2({data: values, width: n, height: m}, r * pow2k); return values; } function density(data) { var values = grid(data), tz = threshold(values), pow4k = Math.pow(2, 2 * k); // Convert number of thresholds into uniform thresholds. if (!Array.isArray(tz)) { tz = ticks(Number.MIN_VALUE, max(values) / pow4k, tz); } return Contours() .size([n, m]) .thresholds(tz.map(d => d * pow4k)) (values) .map((c, i) => (c.value = +tz[i], transform(c))); } density.contours = function(data) { var values = grid(data), contours = Contours().size([n, m]), pow4k = Math.pow(2, 2 * k), contour = value => { value = +value; var c = transform(contours.contour(values, value * pow4k)); c.value = value; // preserve exact threshold value return c; }; Object.defineProperty(contour, "max", {get: () => max(values) / pow4k}); return contour; }; function transform(geometry) { geometry.coordinates.forEach(transformPolygon); return geometry; } function transformPolygon(coordinates) { coordinates.forEach(transformRing); } function transformRing(coordinates) { coordinates.forEach(transformPoint); } // TODO Optimize. function transformPoint(coordinates) { coordinates[0] = coordinates[0] * Math.pow(2, k) - o; coordinates[1] = coordinates[1] * Math.pow(2, k) - o; } function resize() { o = r * 3; n = (dx + o * 2) >> k; m = (dy + o * 2) >> k; return density; } density.x = function(_) { return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), density) : x; }; density.y = function(_) { return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), density) : y; }; density.weight = function(_) { return arguments.length ? (weight = typeof _ === "function" ? _ : constant(+_), density) : weight; }; density.size = function(_) { if (!arguments.length) return [dx, dy]; var _0 = +_[0], _1 = +_[1]; if (!(_0 >= 0 && _1 >= 0)) throw new Error("invalid size"); return dx = _0, dy = _1, resize(); }; density.cellSize = function(_) { if (!arguments.length) return 1 << k; if (!((_ = +_) >= 1)) throw new Error("invalid cell size"); return k = Math.floor(Math.log(_) / Math.LN2), resize(); }; density.thresholds = function(_) { return arguments.length ? (threshold = typeof _ === "function" ? _ : Array.isArray(_) ? constant(slice.call(_)) : constant(_), density) : threshold; }; density.bandwidth = function(_) { if (!arguments.length) return Math.sqrt(r * (r + 1)); if (!((_ = +_) >= 0)) throw new Error("invalid bandwidth"); return r = (Math.sqrt(4 * _ * _ + 1) - 1) / 2, resize(); }; return density; } ================================================ FILE: src/index.js ================================================ export {default as contours} from "./contours.js"; export {default as contourDensity} from "./density.js"; ================================================ FILE: src/noop.js ================================================ export default function() {} ================================================ FILE: test/.eslintrc.json ================================================ { "env": { "mocha": true } } ================================================ FILE: test/asserts.js ================================================ import assert from "assert"; export function assertInDelta(actual, expected, delta = 1e-6) { assert(inDelta(actual, expected, delta)); } function inDelta(actual, expected, delta) { return (Array.isArray(expected) ? inDeltaArray : inDeltaNumber)(actual, expected, delta); } function inDeltaArray(actual, expected, delta) { const n = expected.length; if (actual.length !== n) return false; for (let i = 0; i < n; ++i) if (!inDelta(actual[i], expected[i], delta)) return false; return true; } function inDeltaNumber(actual, expected, delta) { return actual >= expected - delta && actual <= expected + delta; } ================================================ FILE: test/contours-test.js ================================================ import assert from "assert"; import {contours} from "../src/index.js"; it("contours(values) returns the expected result for an empty polygon", () => { 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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": [] } ]); }); it("contours(values) returns the expected result for a simple polygon", () => { 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, 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], [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("contours(values).contour(value) returns the expected result for a simple polygon", () => { const c = contours().size([10, 10]); assert.deepStrictEqual(c.contour([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 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 ], 0.5), { "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("contours.smooth(false)(values) returns the expected result for a simple polygon", () => { const c = contours().smooth(false).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, 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, 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("contours(values) returns the expected result for a polygon with a hole", () => { 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, 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": [ [ [[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]], [[4.5, 7], [4, 6.5], [4, 5.5], [4, 4.5], [4.5, 4], [5, 4.5], [5, 5.5], [5, 6.5], [4.5, 7]] ] ] } ]); }); it("contours(values) returns the expected result for a multipolygon", () => { 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, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 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": [ [ [[5, 7.5], [5, 6.5], [5, 5.5], [5, 4.5], [5, 3.5], [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, 7.5]] ], [ [[7, 7.5], [7, 6.5], [7, 5.5], [7, 4.5], [7, 3.5], [6.5, 3], [6, 3.5], [6, 4.5], [6, 5.5], [6, 6.5], [6, 7.5], [6.5, 8], [7, 7.5]] ] ] } ]); }); it("contours(values) returns the expected result for a multipolygon with holes", () => { 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, 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": [ [ [[4, 5.5], [4, 4.5], [4, 3.5], [3.5, 3], [2.5, 3], [1.5, 3], [1, 3.5], [1, 4.5], [1, 5.5], [1.5, 6], [2.5, 6], [3.5, 6], [4, 5.5]], [[2.5, 5], [2, 4.5], [2.5, 4], [3, 4.5], [2.5, 5]] ], [ [[8, 5.5], [8, 4.5], [8, 3.5], [7.5, 3], [6.5, 3], [5.5, 3], [5, 3.5], [5, 4.5], [5, 5.5], [5.5, 6], [6.5, 6], [7.5, 6], [8, 5.5]], [[6.5, 5], [6, 4.5], [6.5, 4], [7, 4.5], [6.5, 5]] ] ] } ]); }); it("contours.size(…) validates the specified size", () => { assert.deepStrictEqual(contours().size([1, 2]).size(), [1, 2]); assert.deepStrictEqual(contours().size([0, 0]).size(), [0, 0]); assert.deepStrictEqual(contours().size([1.5, 2.5]).size(), [1, 2]); assert.throws(() => void contours().size([0, -1]), /invalid size/); }); it("contours(values) returns the expected thresholds", () => { const c = contours().size([10, 10]).thresholds(20); 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, 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 ]).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/); } }); it("contours(values) uses the expected nice thresholds", () => { assert.deepStrictEqual(contours().size([2, 1]).thresholds(14)([-149.76192742819748, 321.19300631539585]).map((c) => c.value), [-150, -100, -50, 0, 50, 100, 150, 200, 250, 300]); assert.deepStrictEqual(contours().size([2, 1]).thresholds(5)([-149.76192742819748, 321.19300631539585]).map((c) => c.value), [-200, -100, 0, 100, 200, 300]); assert.deepStrictEqual(contours().size([2, 1]).thresholds(14)([149.76192742819748, -321.19300631539585]).map((c) => c.value), [-350, -300, -250, -200, -150, -100, -50, 0, 50, 100]); assert.deepStrictEqual(contours().size([2, 1]).thresholds(5)([149.76192742819748, -321.19300631539585]).map((c) => c.value), [-400, -300, -200, -100, 0, 100]); assert.deepStrictEqual(contours().size([2, 1]).thresholds(12)([-29, 50]).map((c) => c.value), [-30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45]); assert.deepStrictEqual(contours().size([2, 1]).thresholds(10)([-41, 245]).map((c) => c.value), [-50, 0, 50, 100, 150, 200]); assert.deepStrictEqual(contours().size([2, 1]).thresholds(9)([-22, 242]).map((c) => c.value), [-50, 0, 50, 100, 150, 200]); }); ================================================ FILE: test/data/faithful.tsv ================================================ eruptions waiting 3.600 79 1.800 54 3.333 74 2.283 62 4.533 85 2.883 55 4.700 88 3.600 85 1.950 51 4.350 85 1.833 54 3.917 84 4.200 78 1.750 47 4.700 83 2.167 52 1.750 62 4.800 84 1.600 52 4.250 79 1.800 51 1.750 47 3.450 78 3.067 69 4.533 74 3.600 83 1.967 55 4.083 76 3.850 78 4.433 79 4.300 73 4.467 77 3.367 66 4.033 80 3.833 74 2.017 52 1.867 48 4.833 80 1.833 59 4.783 90 4.350 80 1.883 58 4.567 84 1.750 58 4.533 73 3.317 83 3.833 64 2.100 53 4.633 82 2.000 59 4.800 75 4.716 90 1.833 54 4.833 80 1.733 54 4.883 83 3.717 71 1.667 64 4.567 77 4.317 81 2.233 59 4.500 84 1.750 48 4.800 82 1.817 60 4.400 92 4.167 78 4.700 78 2.067 65 4.700 73 4.033 82 1.967 56 4.500 79 4.000 71 1.983 62 5.067 76 2.017 60 4.567 78 3.883 76 3.600 83 4.133 75 4.333 82 4.100 70 2.633 65 4.067 73 4.933 88 3.950 76 4.517 80 2.167 48 4.000 86 2.200 60 4.333 90 1.867 50 4.817 78 1.833 63 4.300 72 4.667 84 3.750 75 1.867 51 4.900 82 2.483 62 4.367 88 2.100 49 4.500 83 4.050 81 1.867 47 4.700 84 1.783 52 4.850 86 3.683 81 4.733 75 2.300 59 4.900 89 4.417 79 1.700 59 4.633 81 2.317 50 4.600 85 1.817 59 4.417 87 2.617 53 4.067 69 4.250 77 1.967 56 4.600 88 3.767 81 1.917 45 4.500 82 2.267 55 4.650 90 1.867 45 4.167 83 2.800 56 4.333 89 1.833 46 4.383 82 1.883 51 4.933 86 2.033 53 3.733 79 4.233 81 2.233 60 4.533 82 4.817 77 4.333 76 1.983 59 4.633 80 2.017 49 5.100 96 1.800 53 5.033 77 4.000 77 2.400 65 4.600 81 3.567 71 4.000 70 4.500 81 4.083 93 1.800 53 3.967 89 2.200 45 4.150 86 2.000 58 3.833 78 3.500 66 4.583 76 2.367 63 5.000 88 1.933 52 4.617 93 1.917 49 2.083 57 4.583 77 3.333 68 4.167 81 4.333 81 4.500 73 2.417 50 4.000 85 4.167 74 1.883 55 4.583 77 4.250 83 3.767 83 2.033 51 4.433 78 4.083 84 1.833 46 4.417 83 2.183 55 4.800 81 1.833 57 4.800 76 4.100 84 3.966 77 4.233 81 3.500 87 4.366 77 2.250 51 4.667 78 2.100 60 4.350 82 4.133 91 1.867 53 4.600 78 1.783 46 4.367 77 3.850 84 1.933 49 4.500 83 2.383 71 4.700 80 1.867 49 3.833 75 3.417 64 4.233 76 2.400 53 4.800 94 2.000 55 4.150 76 1.867 50 4.267 82 1.750 54 4.483 75 4.000 78 4.117 79 4.083 78 4.267 78 3.917 70 4.550 79 4.083 70 2.417 54 4.183 86 2.217 50 4.450 90 1.883 54 1.850 54 4.283 77 3.950 79 2.333 64 4.150 75 2.350 47 4.933 86 2.900 63 4.583 85 3.833 82 2.083 57 4.367 82 2.133 67 4.350 74 2.200 54 4.450 83 3.567 73 4.500 73 4.150 88 3.817 80 3.917 71 4.450 83 2.000 56 4.283 79 4.767 78 4.533 84 1.850 58 4.250 83 1.983 43 2.250 60 4.750 75 4.117 81 2.150 46 4.417 90 1.817 46 4.467 74 ================================================ FILE: test/density-test.js ================================================ import assert from "assert"; import {extent, ticks} from "d3-array"; import {autoType} from "d3-dsv"; import {tsv} from "d3-fetch"; import {polygonCentroid} from "d3-polygon"; import {scaleLinear} from "d3-scale"; import {contourDensity} from "../src/index.js"; import {assertInDelta} from "./asserts.js"; import it from "./jsdom.js"; it("density.size(…) validates the specified size", () => { assert.deepStrictEqual(contourDensity().size([1, 2]).size(), [1, 2]); assert.deepStrictEqual(contourDensity().size([0, 0]).size(), [0, 0]); assert.deepStrictEqual(contourDensity().size([1.5, 2.5]).size(), [1.5, 2.5]); assert.throws(() => void contourDensity().size([0, -1]), /invalid size/); }); it("contourDensity(data) returns the expected result for empty data", () => { const c = contourDensity(); assert.deepStrictEqual(c([]), []); }); it("contourDensity(data) returns contours centered on a point", () => { const c = contourDensity().thresholds([0.00001, 0.0001]); for (const p of [[100, 100], [100.5, 102]]) { const contour = c([p]); assert.strictEqual(contour.length, 2); for (const b of contour) { const a = polygonCentroid(b.coordinates[0][0]); assertInDelta(a[0], p[0], 0.1); assertInDelta(a[1], p[1], 0.1); } } }); it("contourDensity.thresholds(values[])(data) returns contours for the given values", () => { const points = [[1, 0], [0, 1], [1, 1]]; const c = contourDensity(); const c1 = c(points); const values1 = c1.map(d => d.value); const c2 = c.thresholds(values1)(points); const values2 = c2.map(d => d.value); assert.deepStrictEqual(values1, values2); }); it("contourDensity.weight(…) accepts NaN weights", () => { const points = [[1, 0, 1], [0, 1, -2], [1, 1, NaN]]; const c = contourDensity().weight(d => d[2])(points); assert.strictEqual(c.length, 24); }); it("contourDensity.thresholds(values[])(data) returns contours for the given values at a different cellSize", () => { const points = [[1, 0], [0, 1], [1, 1]]; const c = contourDensity().cellSize(16); const c1 = c(points); const values1 = c1.map(d => d.value); const c2 = c.thresholds(values1)(points); const values2 = c2.map(d => d.value); assert.deepStrictEqual(values1, values2); }); it("contourDensity(data) returns nice default thresholds", async () => { const faithful = await tsv("data/faithful.tsv", autoType); const width = 960, height = 500, marginTop = 20, marginRight = 30, marginBottom = 30, marginLeft = 40; const x = scaleLinear() .domain(extent(faithful, d => d.waiting)).nice() .rangeRound([marginLeft, width - marginRight]); const y = scaleLinear() .domain(extent(faithful, d => d.eruptions)).nice() .rangeRound([height - marginBottom, marginTop]); const contour = contourDensity() .x(d => x(d.waiting)) .y(d => y(d.eruptions)) .size([width, height]) .bandwidth(30) (faithful); assert.deepStrictEqual(contour.map(c => c.value), ticks(0.0002, 0.0059, 30)); }); it("contourDensity.contours(data) preserves the specified threshold exactly", async () => { const faithful = await tsv("data/faithful.tsv", autoType); const width = 960, height = 500, marginTop = 20, marginRight = 30, marginBottom = 30, marginLeft = 40; const x = scaleLinear() .domain(extent(faithful, d => d.waiting)).nice() .rangeRound([marginLeft, width - marginRight]); const y = scaleLinear() .domain(extent(faithful, d => d.eruptions)).nice() .rangeRound([height - marginBottom, marginTop]); const contour = contourDensity() .x(d => x(d.waiting)) .y(d => y(d.eruptions)) .size([width, height]) .bandwidth(30) .contours(faithful); for (const value of ticks(0.0002, 0.006, 30)) { assert.strictEqual(contour(value).value, value); } }); ================================================ FILE: test/jsdom.js ================================================ import {promises as fs} from "fs"; import * as path from "path"; import {JSDOM} from "jsdom"; export default function jsdomit(description, run) { return it(description, withJsdom(run)); } jsdomit.skip = (description, run) => { return it.skip(description, withJsdom(run)); }; jsdomit.only = (description, run) => { return it.only(description, withJsdom(run)); }; function withJsdom(run) { return async () => { const jsdom = new JSDOM(""); global.window = jsdom.window; global.document = jsdom.window.document; global.navigator = jsdom.window.navigator; global.Event = jsdom.window.Event; global.Node = jsdom.window.Node; global.NodeList = jsdom.window.NodeList; global.HTMLCollection = jsdom.window.HTMLCollection; global.fetch = async (href) => new Response(path.resolve("./test", href)); try { return await run(); } finally { delete global.window; delete global.document; delete global.navigator; delete global.Event; delete global.Node; delete global.NodeList; delete global.HTMLCollection; delete global.fetch; } }; } class Response { constructor(href) { this._href = href; this.ok = true; this.status = 200; } async text() { return fs.readFile(this._href, {encoding: "utf-8"}); } async json() { return JSON.parse(await this.text()); } } ================================================ FILE: test/snapshot.js ================================================ import assert from "assert"; import {promises as fs} from "fs"; import * as path from "path"; import beautify from "js-beautify"; import it from "./jsdom.js"; import * as snapshots from "./snapshots/index.js"; for (const [name, snapshot] of Object.entries(snapshots)) { it(`snapshot ${name}`, async () => { const svg = await snapshot(); svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.w3.org/2000/svg"); svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); const actual = beautify.html(svg.outerHTML, {indent_size: 2}); const outfile = path.resolve("./test/output", `${path.basename(name, ".js")}.svg`); const diffile = path.resolve("./test/output", `${path.basename(name, ".js")}-changed.svg`); let expected; try { expected = await fs.readFile(outfile, "utf8"); } catch (error) { if (error.code === "ENOENT" && process.env.CI !== "true") { console.warn(`! generating ${outfile}`); await fs.writeFile(outfile, actual, "utf8"); return; } else { throw error; } } if (actual === expected) { if (process.env.CI !== "true") { try { await fs.unlink(diffile); console.warn(`! deleted ${diffile}`); } catch (error) { if (error.code !== "ENOENT") { throw error; } } } } else { console.warn(`! generating ${diffile}`); await fs.writeFile(diffile, actual, "utf8"); } assert(actual === expected, `${name} must match snapshot`); }); } ================================================ FILE: test/snapshots/index.js ================================================ import {extent, ticks} from "d3-array"; import {axisBottom, axisLeft} from "d3-axis"; import {autoType} from "d3-dsv"; import {tsv} from "d3-fetch"; import {geoPath} from "d3-geo"; import {scaleLinear} from "d3-scale"; import {select} from "d3-selection"; import {svg} from "htl"; import {contourDensity} from "d3-contour"; export async function faithfulContours() { const faithful = await tsv("data/faithful.tsv", autoType); const width = 960, height = 500, marginTop = 20, marginRight = 30, marginBottom = 30, marginLeft = 40; const x = scaleLinear() .domain(extent(faithful, d => d.waiting)).nice() .rangeRound([marginLeft, width - marginRight]); const y = scaleLinear() .domain(extent(faithful, d => d.eruptions)).nice() .rangeRound([height - marginBottom, marginTop]); const contours = contourDensity() .x(d => x(d.waiting)) .y(d => y(d.eruptions)) .size([width, height]) .bandwidth(30) .thresholds(30) (faithful); const path = geoPath(); return svg` ${select(svg``).call(axisLeft(y)).call(g => g.select(".domain").remove())} ${select(svg``).call(axisBottom(x)).call(g => g.select(".domain").remove())} ${faithful.map(d => svg``)} ${contours.map(c => svg``)} `; } export async function faithfulContour() { const faithful = await tsv("data/faithful.tsv", autoType); const width = 960, height = 500, marginTop = 20, marginRight = 30, marginBottom = 30, marginLeft = 40; const x = scaleLinear() .domain(extent(faithful, d => d.waiting)).nice() .rangeRound([marginLeft, width - marginRight]); const y = scaleLinear() .domain(extent(faithful, d => d.eruptions)).nice() .rangeRound([height - marginBottom, marginTop]); const contour = contourDensity() .x(d => x(d.waiting)) .y(d => y(d.eruptions)) .size([width, height]) .bandwidth(30) .contours(faithful); const thresholds = ticks(0, contour.max, 30).slice(1); const path = geoPath(); return svg` ${select(svg``).call(axisLeft(y)).call(g => g.select(".domain").remove())} ${select(svg``).call(axisBottom(x)).call(g => g.select(".domain").remove())} ${faithful.map(d => svg``)} ${thresholds.map(t => svg``)} `; }