Repository: d3/d3-force Branch: main Commit: c3e73cf64181 Files: 27 Total size: 30.6 KB Directory structure: gitextract_xmeu_jkh/ ├── .eslintrc.json ├── .github/ │ ├── eslint.json │ └── workflows/ │ └── node.js.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── rollup.config.js ├── src/ │ ├── center.js │ ├── collide.js │ ├── constant.js │ ├── index.js │ ├── jiggle.js │ ├── lcg.js │ ├── link.js │ ├── manyBody.js │ ├── radial.js │ ├── simulation.js │ ├── x.js │ └── y.js └── test/ ├── .eslintrc.json ├── asserts.js ├── center-test.js ├── collide-test.js ├── find-test.js ├── simulation-test.js └── x-test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "eslint:recommended", "parserOptions": { "sourceType": "module", "ecmaVersion": 8 }, "env": { "es6": true }, "rules": { "no-cond-assign": 0 } } ================================================ 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 2010-2021 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-force This module implements a [velocity Verlet](https://en.wikipedia.org/wiki/Verlet_integration) numerical integrator for simulating physical forces on particles. Force simulations can be used to visualize [networks](https://observablehq.com/@d3/force-directed-graph/2?intent=fork) and [hierarchies](https://observablehq.com/@d3/force-directed-tree?intent=fork), and to resolve [collisions](./d3-force/collide.md) as in [bubble charts](http://www.nytimes.com/interactive/2012/09/06/us/politics/convention-word-counts.html). ## Resources * [Documentation](https://d3js.org/d3-force) * [Examples](https://observablehq.com/collection/@d3/d3-force) * [Releases](https://github.com/d3/d3-force/releases) * [Getting help](https://d3js.org/community) ================================================ FILE: package.json ================================================ { "name": "d3-force", "version": "3.0.0", "description": "Force-directed graph layout using velocity Verlet integration.", "homepage": "https://d3js.org/d3-force/", "repository": { "type": "git", "url": "https://github.com/d3/d3-force.git" }, "keywords": [ "d3", "d3-module", "layout", "network", "graph", "force", "verlet", "infovis" ], "license": "ISC", "author": { "name": "Mike Bostock", "url": "https://bost.ocks.org/mike" }, "type": "module", "files": [ "src/**/*.js", "dist/**/*.js" ], "module": "src/index.js", "main": "src/index.js", "jsdelivr": "dist/d3-force.min.js", "unpkg": "dist/d3-force.min.js", "exports": { "umd": "./dist/d3-force.min.js", "default": "./src/index.js" }, "sideEffects": false, "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" }, "devDependencies": { "eslint": "7", "mocha": "8", "rollup": "2", "rollup-plugin-terser": "7" }, "scripts": { "test": "mocha 'test/**/*-test.js' && eslint src test", "prepublishOnly": "rm -rf dist && yarn test && 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 * as meta from "./package.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/center.js ================================================ export default function(x, y) { var nodes, strength = 1; if (x == null) x = 0; if (y == null) y = 0; function force() { var i, n = nodes.length, node, sx = 0, sy = 0; for (i = 0; i < n; ++i) { node = nodes[i], sx += node.x, sy += node.y; } for (sx = (sx / n - x) * strength, sy = (sy / n - y) * strength, i = 0; i < n; ++i) { node = nodes[i], node.x -= sx, node.y -= sy; } } force.initialize = function(_) { nodes = _; }; force.x = function(_) { return arguments.length ? (x = +_, force) : x; }; force.y = function(_) { return arguments.length ? (y = +_, force) : y; }; force.strength = function(_) { return arguments.length ? (strength = +_, force) : strength; }; return force; } ================================================ FILE: src/collide.js ================================================ import {quadtree} from "d3-quadtree"; import constant from "./constant.js"; import jiggle from "./jiggle.js"; function x(d) { return d.x + d.vx; } function y(d) { return d.y + d.vy; } export default function(radius) { var nodes, radii, random, strength = 1, iterations = 1; if (typeof radius !== "function") radius = constant(radius == null ? 1 : +radius); function force() { var i, n = nodes.length, tree, node, xi, yi, ri, ri2; for (var k = 0; k < iterations; ++k) { tree = quadtree(nodes, x, y).visitAfter(prepare); for (i = 0; i < n; ++i) { node = nodes[i]; ri = radii[node.index], ri2 = ri * ri; xi = node.x + node.vx; yi = node.y + node.vy; tree.visit(apply); } } function apply(quad, x0, y0, x1, y1) { var data = quad.data, rj = quad.r, r = ri + rj; if (data) { if (data.index > node.index) { var x = xi - data.x - data.vx, y = yi - data.y - data.vy, l = x * x + y * y; if (l < r * r) { if (x === 0) x = jiggle(random), l += x * x; if (y === 0) y = jiggle(random), l += y * y; l = (r - (l = Math.sqrt(l))) / l * strength; node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj)); node.vy += (y *= l) * r; data.vx -= x * (r = 1 - r); data.vy -= y * r; } } return; } return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r; } } function prepare(quad) { if (quad.data) return quad.r = radii[quad.data.index]; for (var i = quad.r = 0; i < 4; ++i) { if (quad[i] && quad[i].r > quad.r) { quad.r = quad[i].r; } } } function initialize() { if (!nodes) return; var i, n = nodes.length, node; radii = new Array(n); for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes); } force.initialize = function(_nodes, _random) { nodes = _nodes; random = _random; initialize(); }; force.iterations = function(_) { return arguments.length ? (iterations = +_, force) : iterations; }; force.strength = function(_) { return arguments.length ? (strength = +_, force) : strength; }; force.radius = function(_) { return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius; }; return force; } ================================================ FILE: src/constant.js ================================================ export default function(x) { return function() { return x; }; } ================================================ FILE: src/index.js ================================================ export {default as forceCenter} from "./center.js"; export {default as forceCollide} from "./collide.js"; export {default as forceLink} from "./link.js"; export {default as forceManyBody} from "./manyBody.js"; export {default as forceRadial} from "./radial.js"; export {default as forceSimulation} from "./simulation.js"; export {default as forceX} from "./x.js"; export {default as forceY} from "./y.js"; ================================================ FILE: src/jiggle.js ================================================ export default function(random) { return (random() - 0.5) * 1e-6; } ================================================ FILE: src/lcg.js ================================================ // https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use const a = 1664525; const c = 1013904223; const m = 4294967296; // 2^32 export default function() { let s = 1; return () => (s = (a * s + c) % m) / m; } ================================================ FILE: src/link.js ================================================ import constant from "./constant.js"; import jiggle from "./jiggle.js"; function index(d) { return d.index; } function find(nodeById, nodeId) { var node = nodeById.get(nodeId); if (!node) throw new Error("node not found: " + nodeId); return node; } export default function(links) { var id = index, strength = defaultStrength, strengths, distance = constant(30), distances, nodes, count, bias, random, iterations = 1; if (links == null) links = []; function defaultStrength(link) { return 1 / Math.min(count[link.source.index], count[link.target.index]); } function force(alpha) { for (var k = 0, n = links.length; k < iterations; ++k) { for (var i = 0, link, source, target, x, y, l, b; i < n; ++i) { link = links[i], source = link.source, target = link.target; x = target.x + target.vx - source.x - source.vx || jiggle(random); y = target.y + target.vy - source.y - source.vy || jiggle(random); l = Math.sqrt(x * x + y * y); l = (l - distances[i]) / l * alpha * strengths[i]; x *= l, y *= l; target.vx -= x * (b = bias[i]); target.vy -= y * b; source.vx += x * (b = 1 - b); source.vy += y * b; } } } function initialize() { if (!nodes) return; var i, n = nodes.length, m = links.length, nodeById = new Map(nodes.map((d, i) => [id(d, i, nodes), d])), link; for (i = 0, count = new Array(n); i < m; ++i) { link = links[i], link.index = i; if (typeof link.source !== "object") link.source = find(nodeById, link.source); if (typeof link.target !== "object") link.target = find(nodeById, link.target); count[link.source.index] = (count[link.source.index] || 0) + 1; count[link.target.index] = (count[link.target.index] || 0) + 1; } for (i = 0, bias = new Array(m); i < m; ++i) { link = links[i], bias[i] = count[link.source.index] / (count[link.source.index] + count[link.target.index]); } strengths = new Array(m), initializeStrength(); distances = new Array(m), initializeDistance(); } function initializeStrength() { if (!nodes) return; for (var i = 0, n = links.length; i < n; ++i) { strengths[i] = +strength(links[i], i, links); } } function initializeDistance() { if (!nodes) return; for (var i = 0, n = links.length; i < n; ++i) { distances[i] = +distance(links[i], i, links); } } force.initialize = function(_nodes, _random) { nodes = _nodes; random = _random; initialize(); }; force.links = function(_) { return arguments.length ? (links = _, initialize(), force) : links; }; force.id = function(_) { return arguments.length ? (id = _, force) : id; }; force.iterations = function(_) { return arguments.length ? (iterations = +_, force) : iterations; }; force.strength = function(_) { return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initializeStrength(), force) : strength; }; force.distance = function(_) { return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance; }; return force; } ================================================ FILE: src/manyBody.js ================================================ import {quadtree} from "d3-quadtree"; import constant from "./constant.js"; import jiggle from "./jiggle.js"; import {x, y} from "./simulation.js"; export default function() { var nodes, node, random, alpha, strength = constant(-30), strengths, distanceMin2 = 1, distanceMax2 = Infinity, theta2 = 0.81; function force(_) { var i, n = nodes.length, tree = quadtree(nodes, x, y).visitAfter(accumulate); for (alpha = _, i = 0; i < n; ++i) node = nodes[i], tree.visit(apply); } function initialize() { if (!nodes) return; var i, n = nodes.length, node; strengths = new Array(n); for (i = 0; i < n; ++i) node = nodes[i], strengths[node.index] = +strength(node, i, nodes); } function accumulate(quad) { var strength = 0, q, c, weight = 0, x, y, i; // For internal nodes, accumulate forces from child quadrants. if (quad.length) { for (x = y = i = 0; i < 4; ++i) { if ((q = quad[i]) && (c = Math.abs(q.value))) { strength += q.value, weight += c, x += c * q.x, y += c * q.y; } } quad.x = x / weight; quad.y = y / weight; } // For leaf nodes, accumulate forces from coincident quadrants. else { q = quad; q.x = q.data.x; q.y = q.data.y; do strength += strengths[q.data.index]; while (q = q.next); } quad.value = strength; } function apply(quad, x1, _, x2) { if (!quad.value) return true; var x = quad.x - node.x, y = quad.y - node.y, w = x2 - x1, l = x * x + y * y; // Apply the Barnes-Hut approximation if possible. // Limit forces for very close nodes; randomize direction if coincident. if (w * w / theta2 < l) { if (l < distanceMax2) { if (x === 0) x = jiggle(random), l += x * x; if (y === 0) y = jiggle(random), l += y * y; if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l); node.vx += x * quad.value * alpha / l; node.vy += y * quad.value * alpha / l; } return true; } // Otherwise, process points directly. else if (quad.length || l >= distanceMax2) return; // Limit forces for very close nodes; randomize direction if coincident. if (quad.data !== node || quad.next) { if (x === 0) x = jiggle(random), l += x * x; if (y === 0) y = jiggle(random), l += y * y; if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l); } do if (quad.data !== node) { w = strengths[quad.data.index] * alpha / l; node.vx += x * w; node.vy += y * w; } while (quad = quad.next); } force.initialize = function(_nodes, _random) { nodes = _nodes; random = _random; initialize(); }; force.strength = function(_) { return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength; }; force.distanceMin = function(_) { return arguments.length ? (distanceMin2 = _ * _, force) : Math.sqrt(distanceMin2); }; force.distanceMax = function(_) { return arguments.length ? (distanceMax2 = _ * _, force) : Math.sqrt(distanceMax2); }; force.theta = function(_) { return arguments.length ? (theta2 = _ * _, force) : Math.sqrt(theta2); }; return force; } ================================================ FILE: src/radial.js ================================================ import constant from "./constant.js"; export default function(radius, x, y) { var nodes, strength = constant(0.1), strengths, radiuses; if (typeof radius !== "function") radius = constant(+radius); if (x == null) x = 0; if (y == null) y = 0; function force(alpha) { for (var i = 0, n = nodes.length; i < n; ++i) { var node = nodes[i], dx = node.x - x || 1e-6, dy = node.y - y || 1e-6, r = Math.sqrt(dx * dx + dy * dy), k = (radiuses[i] - r) * strengths[i] * alpha / r; node.vx += dx * k; node.vy += dy * k; } } function initialize() { if (!nodes) return; var i, n = nodes.length; strengths = new Array(n); radiuses = new Array(n); for (i = 0; i < n; ++i) { radiuses[i] = +radius(nodes[i], i, nodes); strengths[i] = isNaN(radiuses[i]) ? 0 : +strength(nodes[i], i, nodes); } } force.initialize = function(_) { nodes = _, initialize(); }; force.strength = function(_) { return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength; }; force.radius = function(_) { return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius; }; force.x = function(_) { return arguments.length ? (x = +_, force) : x; }; force.y = function(_) { return arguments.length ? (y = +_, force) : y; }; return force; } ================================================ FILE: src/simulation.js ================================================ import {dispatch} from "d3-dispatch"; import {timer} from "d3-timer"; import lcg from "./lcg.js"; export function x(d) { return d.x; } export function y(d) { return d.y; } var initialRadius = 10, initialAngle = Math.PI * (3 - Math.sqrt(5)); export default function(nodes) { var simulation, alpha = 1, alphaMin = 0.001, alphaDecay = 1 - Math.pow(alphaMin, 1 / 300), alphaTarget = 0, velocityDecay = 0.6, forces = new Map(), stepper = timer(step), event = dispatch("tick", "end"), random = lcg(); if (nodes == null) nodes = []; function step() { tick(); event.call("tick", simulation); if (alpha < alphaMin) { stepper.stop(); event.call("end", simulation); } } function tick(iterations) { var i, n = nodes.length, node; if (iterations === undefined) iterations = 1; for (var k = 0; k < iterations; ++k) { alpha += (alphaTarget - alpha) * alphaDecay; forces.forEach(function(force) { force(alpha); }); for (i = 0; i < n; ++i) { node = nodes[i]; if (node.fx == null) node.x += node.vx *= velocityDecay; else node.x = node.fx, node.vx = 0; if (node.fy == null) node.y += node.vy *= velocityDecay; else node.y = node.fy, node.vy = 0; } } return simulation; } function initializeNodes() { for (var i = 0, n = nodes.length, node; i < n; ++i) { node = nodes[i], node.index = i; if (node.fx != null) node.x = node.fx; if (node.fy != null) node.y = node.fy; if (isNaN(node.x) || isNaN(node.y)) { var radius = initialRadius * Math.sqrt(0.5 + i), angle = i * initialAngle; node.x = radius * Math.cos(angle); node.y = radius * Math.sin(angle); } if (isNaN(node.vx) || isNaN(node.vy)) { node.vx = node.vy = 0; } } } function initializeForce(force) { if (force.initialize) force.initialize(nodes, random); return force; } initializeNodes(); return simulation = { tick: tick, restart: function() { return stepper.restart(step), simulation; }, stop: function() { return stepper.stop(), simulation; }, nodes: function(_) { return arguments.length ? (nodes = _, initializeNodes(), forces.forEach(initializeForce), simulation) : nodes; }, alpha: function(_) { return arguments.length ? (alpha = +_, simulation) : alpha; }, alphaMin: function(_) { return arguments.length ? (alphaMin = +_, simulation) : alphaMin; }, alphaDecay: function(_) { return arguments.length ? (alphaDecay = +_, simulation) : +alphaDecay; }, alphaTarget: function(_) { return arguments.length ? (alphaTarget = +_, simulation) : alphaTarget; }, velocityDecay: function(_) { return arguments.length ? (velocityDecay = 1 - _, simulation) : 1 - velocityDecay; }, randomSource: function(_) { return arguments.length ? (random = _, forces.forEach(initializeForce), simulation) : random; }, force: function(name, _) { return arguments.length > 1 ? ((_ == null ? forces.delete(name) : forces.set(name, initializeForce(_))), simulation) : forces.get(name); }, find: function(x, y, radius) { var i = 0, n = nodes.length, dx, dy, d2, node, closest; if (radius == null) radius = Infinity; else radius *= radius; for (i = 0; i < n; ++i) { node = nodes[i]; dx = x - node.x; dy = y - node.y; d2 = dx * dx + dy * dy; if (d2 < radius) closest = node, radius = d2; } return closest; }, on: function(name, _) { return arguments.length > 1 ? (event.on(name, _), simulation) : event.on(name); } }; } ================================================ FILE: src/x.js ================================================ import constant from "./constant.js"; export default function(x) { var strength = constant(0.1), nodes, strengths, xz; if (typeof x !== "function") x = constant(x == null ? 0 : +x); function force(alpha) { for (var i = 0, n = nodes.length, node; i < n; ++i) { node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha; } } function initialize() { if (!nodes) return; var i, n = nodes.length; strengths = new Array(n); xz = new Array(n); for (i = 0; i < n; ++i) { strengths[i] = isNaN(xz[i] = +x(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes); } } force.initialize = function(_) { nodes = _; initialize(); }; force.strength = function(_) { return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength; }; force.x = function(_) { return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), initialize(), force) : x; }; return force; } ================================================ FILE: src/y.js ================================================ import constant from "./constant.js"; export default function(y) { var strength = constant(0.1), nodes, strengths, yz; if (typeof y !== "function") y = constant(y == null ? 0 : +y); function force(alpha) { for (var i = 0, n = nodes.length, node; i < n; ++i) { node = nodes[i], node.vy += (yz[i] - node.y) * strengths[i] * alpha; } } function initialize() { if (!nodes) return; var i, n = nodes.length; strengths = new Array(n); yz = new Array(n); for (i = 0; i < n; ++i) { strengths[i] = isNaN(yz[i] = +y(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes); } } force.initialize = function(_) { nodes = _; initialize(); }; force.strength = function(_) { return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength; }; force.y = function(_) { return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), initialize(), force) : y; }; return force; } ================================================ FILE: test/.eslintrc.json ================================================ { "extends": "eslint:recommended", "parserOptions": { "sourceType": "module", "ecmaVersion": 8 }, "env": { "es6": true, "mocha": true } } ================================================ FILE: test/asserts.js ================================================ import assert from "assert"; export function assertNodeEqual(actual, expected, delta = 1e-6) { assert(nodeEqual(actual, expected, delta), `${actual} and ${expected} should be similar`); } function nodeEqual(actual, expected, delta) { return actual.index == expected.index && Math.abs(actual.x - expected.x) < delta && Math.abs(actual.vx - expected.vx) < delta && Math.abs(actual.y - expected.y) < delta && Math.abs(actual.vy - expected.vy) < delta && !(Math.abs(actual.fx - expected.fx) > delta) && !(Math.abs(actual.fy - expected.fy) > delta); } ================================================ FILE: test/center-test.js ================================================ import {forceCenter, forceSimulation} from "../src/index.js"; import {assertNodeEqual} from "./asserts.js"; it("forceCenter repositions nodes", () => { const center = forceCenter(0, 0); const f = forceSimulation().force("center", center).stop(); const a = {x: 100, y: 0}, b = {x: 200, y: 0}, c = {x: 300, y: 0}; f.nodes([a, b, c]); f.tick(); assertNodeEqual(a, {index: 0, x: -100, y: 0, vy: 0, vx: 0}); assertNodeEqual(b, {index: 1, x: 0, y: 0, vy: 0, vx: 0}); assertNodeEqual(c, {index: 2, x: 100, y: 0, vy: 0, vx: 0}); }); it("forceCenter respects fixed positions", () => { const center = forceCenter(); const f = forceSimulation().force("center", center).stop(); const a = {fx: 0, fy: 0}, b = {}, c = {}; f.nodes([a, b, c]); f.tick(); assertNodeEqual(a, {fx: 0, fy: 0, index: 0, x: 0, y: 0, vy: 0, vx: 0}); }); ================================================ FILE: test/collide-test.js ================================================ import assert from "assert"; import {forceCollide, forceSimulation} from "../src/index.js"; import {assertNodeEqual} from "./asserts.js"; it("forceCollide collides nodes", () => { const collide = forceCollide(1); const f = forceSimulation().force("collide", collide).stop(); const a = {}, b = {}, c = {}; f.nodes([a, b, c]); f.tick(10); assertNodeEqual(a, {index: 0, x: 7.0710678118654755, y: 0, vy: 0, vx: 0}); assertNodeEqual(b, {index: 1, x: -9.03088751750192, y: 8.27303273571596, vy: 0, vx: 0}); assertNodeEqual(c, {index: 2, x: 1.3823220809823638, y: -15.750847141167634, vy: 0, vx: 0}); collide.radius(100); f.tick(10); assertNodeEqual(a, {index: 0, x: 174.08616723117228, y: 66.51743051995625, vy: 0.26976816231064354, vx: 0.677346615710878}); assertNodeEqual(b, {index: 1, x: -139.73606544743998, y: 95.69860503079263, vy: 0.3545632444404687, vx: -0.5300880593105067}); assertNodeEqual(c, {index: 2, x: -34.9275994083864, y: -169.69384995620052, vy: -0.6243314067511122, vx: -0.1472585564003713}); }); it("forceCollide respects fixed positions", () => { const collide = forceCollide(1); const f = forceSimulation().force("collide", collide).stop(); const a = {fx: 0, fy: 0}, b = {}, c = {}; f.nodes([a, b, c]); f.tick(10); assertNodeEqual(a, {fx: 0, fy: 0, index: 0, x: 0, y: 0, vy: 0, vx: 0}); collide.radius(100); f.tick(10); assertNodeEqual(a, {fx: 0, fy: 0, index: 0, x: 0, y: 0, vy: 0, vx: 0}); }); it("forceCollide jiggles equal positions", () => { const collide = forceCollide(1); const f = forceSimulation().force("collide", collide).stop(); const a = {x: 0, y: 0}, b = {x: 0, y: 0}; f.nodes([a, b]); f.tick(); assert(a.x !== b.x); assert(a.y !== b.y); assert.strictEqual(a.vx, -b.vx); assert.strictEqual(a.vy, -b.vy); }); it("forceCollide jiggles in a reproducible way", () => { const nodes = Array.from({length:10}, () => ({x: 0, y: 0})); forceSimulation().nodes(nodes).force("collide", forceCollide()).stop().tick(50); assertNodeEqual(nodes[0], {x: -5.371433857229194, y: -2.6644608278592576, index: 0, vy: 0, vx: 0}); }); ================================================ FILE: test/find-test.js ================================================ import assert from "assert"; import {forceSimulation} from "../src/index.js"; it("simulation.find finds a node", () => { const f = forceSimulation().stop(); const a = {x: 5, y: 0}, b = {x: 10, y: 16}, c = {x: -10, y: -4}; f.nodes([a, b, c]); assert.strictEqual(f.find(0, 0), a); assert.strictEqual(f.find(0, 20), b); }); it("simulation.find(x, y, radius) finds a node within radius", () => { const f = forceSimulation().stop(); const a = {x: 5, y: 0}, b = {x: 10, y: 16}, c = {x: -10, y: -4}; f.nodes([a, b, c]); assert.strictEqual(f.find(0, 0), a); assert.strictEqual(f.find(0, 0, 1), undefined); assert.strictEqual(f.find(0, 20), b); }); ================================================ FILE: test/simulation-test.js ================================================ import assert from "assert"; import {forceSimulation} from "../src/index.js"; import {assertNodeEqual} from "./asserts.js"; it("forceSimulation() returns a simulation", () => { const f = forceSimulation().stop(); assert.deepStrictEqual(Object.keys(f).sort(), [ 'alpha', 'alphaDecay', 'alphaMin', 'alphaTarget', 'find', 'force', 'nodes', 'on', 'randomSource', 'restart', 'stop', 'tick', 'velocityDecay' ]); }); it("simulation.nodes(nodes) initializes a simulation with indices & phyllotaxis positions, 0 speed", () => { const f = forceSimulation().stop(); const a = {}, b = {}, c = {}; f.nodes([a, b, c]); assertNodeEqual(a, {index: 0, x: 7.0710678118654755, y: 0, vy: 0, vx: 0}); assertNodeEqual(b, {index: 1, x: -9.03088751750192, y: 8.27303273571596, vy: 0, vx: 0}); assertNodeEqual(c, {index: 2, x: 1.3823220809823638, y: -15.750847141167634, vy: 0, vx: 0}); }); ================================================ FILE: test/x-test.js ================================================ import assert from "assert"; import {forceSimulation, forceX, forceY} from "../src/index.js"; import {assertNodeEqual} from "./asserts.js"; it("forceX centers nodes", () => { const x = forceX(200); const f = forceSimulation().force("x", x).stop(); const a = { x: 100, y: 0 }, b = { x: 200, y: 0 }, c = { x: 300, y: 0 }; f.nodes([a, b, c]); f.tick(30); assert(a.x > 190); assert(a.vx > 0); assert.strictEqual(b.x, 200); assert.strictEqual(b.vx, 0); assert(c.x < 210); assert(c.vx < 0); }); it("forceY centers nodes", () => { const y = forceY(200); const f = forceSimulation().force("y", y).stop(); const a = { y: 100, x: 0 }, b = { y: 200, x: 0 }, c = { y: 300, x: 0 }; f.nodes([a, b, c]); f.tick(30); assert(a.y > 190); assert(a.vy > 0); assert.strictEqual(b.y, 200); assert.strictEqual(b.vy, 0); assert(c.y < 210); assert(c.vy < 0); }); it("forceX respects fixed positions", () => { const x = forceX(200); const f = forceSimulation().force("x", x).stop(); const a = { fx: 0, fy:0 }, b = {}, c = {}; f.nodes([a, b, c]); f.tick(); assertNodeEqual(a, { fx: 0, fy: 0, index: 0, x: 0, y: 0, vy: 0, vx: 0 }); }); it("forceY respects fixed positions", () => { const y = forceX(200); const f = forceSimulation().force("y", y).stop(); const a = { fx: 0, fy:0 }, b = {}, c = {}; f.nodes([a, b, c]); f.tick(); assertNodeEqual(a, { fx: 0, fy: 0, index: 0, x: 0, y: 0, vy: 0, vx: 0 }); }); it("forceX.x() accessor", () => { const x = forceX().x(d => d.x0); const f = forceSimulation().force("x", x).stop(); const a = { x: 100, y: 0, x0: 300 }, b = { x: 200, y: 0, x0: 200 }, c = { x: 300, y: 0, x0: 100 }; f.nodes([a, b, c]); f.tick(30); assert(a.x > 290); assert(a.vx > 0); assert.strictEqual(b.x, 200); assert.strictEqual(b.vx, 0); assert(c.x < 110); assert(c.vx < 0); }); it("forceY.y() accessor", () => { const y = forceY().y(d => d.y0); const f = forceSimulation().force("y", y).stop(); const a = { y: 100, x: 0, y0: 300 }, b = { y: 200, x: 0, y0: 200 }, c = { y: 300, x: 0, y0: 100 }; f.nodes([a, b, c]); f.tick(30); assert(a.y > 290); assert(a.vy > 0); assert.strictEqual(b.y, 200); assert.strictEqual(b.vy, 0); assert(c.y < 110); assert(c.vy < 0); });