diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..d1da9f4 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,8 @@ +parserOptions: + sourceType: module + +extends: + "eslint:recommended" + +rules: + no-cond-assign: 0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3070b2e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.sublime-workspace +.DS_Store +build/ +node_modules +npm-debug.log \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..dfb770e --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +*.sublime-* +build/*.zip +test/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..19c1ee6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2018, Two Six Labs, LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5914f4 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# d3-force-reuse diff --git a/index.js b/index.js new file mode 100644 index 0000000..f4a0029 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +export {default as forceManyBodyReuse} from "./src/manyBodyReuse"; diff --git a/package.json b/package.json new file mode 100644 index 0000000..0145247 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "d3-force-reuse", + "version": "0.0.1", + "description": "Fast force-directed graph layout by reusing Barnes Hut approximations.", + "keywords": [ + "d3", + "d3-module", + "layout", + "network", + "graph", + "force", + "verlet", + "infovis" + ], + "homepage": "https://github.com/twosixlabs/d3-force-reuse/", + "license": "BSD-3-Clause", + "author": { + "name": "Robert Gove", + "url": "http://rpgove.com" + }, + "main": "build/d3-force-reuse.js", + "module": "index", + "jsnext:main": "index", + "repository": { + "type": "git", + "url": "https://github.com/twosixlabs/d3-force-reuse.git" + }, + "scripts": { + "pretest": "rm -rf build && mkdir build && rollup -c --banner \"$(preamble)\"", + "test": "tape 'test/**/*-test.js' && eslint index.js src", + "prepare": "npm run test && uglifyjs -b beautify=false,preamble=\"'$(preamble)'\" build/d3-force-reuse.js -c -m -o build/d3-force-reuse.min.js", + "postpublish": "git push && git push --tags && zip -j build/d3-force-reuse.zip -- LICENSE README.md build/d3-force-reuse.js build/d3-force-reuse.min.js" + }, + "devDependencies": { + "eslint": "4", + "package-preamble": "0.1", + "rollup": "0.50", + "tape": "4", + "uglify-js": "3" + }, + "dependencies": { + "d3-quadtree": "^1.0.3" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..448a9b7 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,11 @@ +const definition = require("./package.json"); + +export default { + input: "index", + output: { + extend: true, + file: `build/${definition.name}.js`, + format: "umd", + name: "d3" + } +}; diff --git a/src/constant.js b/src/constant.js new file mode 100644 index 0000000..b7d42e7 --- /dev/null +++ b/src/constant.js @@ -0,0 +1,5 @@ +export default function(x) { + return function() { + return x; + }; +} diff --git a/src/manyBodyReuse.js b/src/manyBodyReuse.js new file mode 100644 index 0000000..1539df7 --- /dev/null +++ b/src/manyBodyReuse.js @@ -0,0 +1,157 @@ +import constant from "./constant"; +import {quadtree} from 'd3-quadtree'; + +export default function() { + var nodes, + node, + alpha, + iter = 0, + tree, + updateClosure, + updateBH, + strength = constant(-30), + strengths, + distanceMin2 = 1, + distanceMax2 = Infinity, + theta2 = 0.81; + + function jiggle() { + return (Math.random() - 0.5) * 1e-6; + } + + function x(d) { + return d.x; + } + + function y(d) { + return d.y; + } + + updateClosure = function () { + return function (i, nodes) { + if (i % 13 === 0) { + return true; + } else { + return false; + } + }; + } + + function force(_) { + var i, n = nodes.length; + if (!tree || updateBH(iter, nodes)) { + tree = quadtree(nodes, x, y).visitAfter(accumulate); + nodes.update.push(iter); + } + for (alpha = _, i = 0; i < n; ++i) node = nodes[i], tree.visit(apply); + ++iter; + } + + function initialize() { + if (!nodes) return; + iter = 0; + nodes.update = []; + updateBH = updateClosure(); + tree = null; + 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(), l += x * x; + if (y === 0) y = jiggle(), 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(), l += x * x; + if (y === 0) y = jiggle(), l += y * y; + if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l); + } + + do if (quad.data !== node) { + // Use the coordinates of the node and not the quad region. + x = quad.data.x - node.x; + y = quad.data.y - node.y; + l = x * x + y * y; + + w = strengths[quad.data.index] * alpha / l; + + node.vx += x * w; + node.vy += y * w; + } while (quad = quad.next); + } + + force.initialize = function(_) { + nodes = _; + 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); + }; + + force.update = function(_) { + return arguments.length ? (updateClosure = _, updateBH = updateClosure(), force) : updateClosure; + }; + + return force; +} \ No newline at end of file