Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 85 additions & 58 deletions README.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
"dist/**/*.js"
],
"dependencies": {
"d3-dispatch": "1",
"d3-quadtree": "1",
"d3-timer": "1"
"d3-dispatch": ">=2.0.0-rc.1",
"d3-quadtree": ">=2.0.0-rc.1",
"d3-timer": ">=2.0.0-rc.1"
},
"sideEffects": false,
"devDependencies": {
Expand Down
8 changes: 6 additions & 2 deletions src/center.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default function(x, y) {
var nodes;
var nodes, strength = 1;

if (x == null) x = 0;
if (y == null) y = 0;
Expand All @@ -15,7 +15,7 @@ export default function(x, y) {
node = nodes[i], sx += node.x, sy += node.y;
}

for (sx = sx / n - x, sy = sy / n - y, i = 0; i < n; ++i) {
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;
}
}
Expand All @@ -32,5 +32,9 @@ export default function(x, y) {
return arguments.length ? (y = +_, force) : y;
};

force.strength = function(_) {
return arguments.length ? (strength = +_, force) : strength;
};

return force;
}
10 changes: 8 additions & 2 deletions src/jiggle.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export default function() {
return (Math.random() - 0.5) * 1e-6;
// https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use
const a = 1664525,
c = 1013904223,
m = 4294967296;
let s = 1;
export default function(seed) {
if (seed) s = Math.abs(a * seed);
return ((s = (a * s + c) % m) / m - 0.5) * 1e-6;
}
2 changes: 2 additions & 0 deletions src/math.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export var pi = Math.PI;
export var radians = pi / 180;
74 changes: 56 additions & 18 deletions src/radial.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,69 @@
import constant from "./constant.js";
import {radians} from "./math.js";

export default function(radius, x, y) {
function value(x) {
if (typeof x === "function") return x;
if (x === null || x === undefined || isNaN(x = +x)) return;
return constant(x);
}

export default function(radius, x, y, angle) {
var nodes,
strength = constant(0.1),
strengths,
radiuses;
radii,
xs,
ys,
angles;

if (typeof radius !== "function") radius = constant(+radius);
if (x == null) x = 0;
if (y == null) y = 0;
radius = value(radius);
x = value(x) || constant(0);
y = value(y) || constant(0);
angle = value(angle);

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;
dx = node.x - xs[i] || 1e-6,
dy = node.y - ys[i] || 1e-6,
r = Math.sqrt(dx * dx + dy * dy);

if (radius) {
var k = ((radii[i] - r) * strengths[i] * alpha) / r;
node.vx += dx * k;
node.vy += dy * k;
}

if (angle) {
var a = Math.atan2(dy, dx),
diff = angles[i] - a,
q = r * Math.sin(diff) * (strengths[i] * alpha);

// the factor below augments the "unease" for points that are opposite
// the correct direction: in that case, though sin(diff) is small,
// tan(diff/2) is very high
q *= Math.hypot(1, Math.tan(diff / 2));

node.vx += -q * Math.sin(a);
node.vy += q * Math.cos(a);
}
}
}

function initialize() {
if (!nodes) return;
var i, n = nodes.length;
strengths = new Array(n);
radiuses = new Array(n);
radii = new Array(n);
xs = new Array(n);
ys = new Array(n);
angles = 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);
if (radius) radii[i] = +radius(nodes[i], i, nodes);
xs[i] = +x(nodes[i], i, nodes);
ys[i] = +y(nodes[i], i, nodes);
if (angle) angles[i] = +angle(nodes[i], i, nodes) * radians;
strengths[i] = isNaN(radii[i]) ? 0 : +strength(nodes[i], i, nodes);
}
}

Expand All @@ -38,19 +72,23 @@ export default function(radius, x, y) {
};

force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
return arguments.length ? (strength = value(_) || constant(1), initialize(), force) : strength;
};

force.radius = function(_) {
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius;
return arguments.length ? (radius = value(_), initialize(), force) : radius;
};

force.x = function(_) {
return arguments.length ? (x = +_, force) : x;
return arguments.length ? (x = value(_) || constant(0), initialize(), force) : x;
};

force.y = function(_) {
return arguments.length ? (y = +_, force) : y;
return arguments.length ? (y = value(_) || constant(0), initialize(), force) : y;
};

force.angle = function(_) {
return arguments.length ? (angle = value(_), initialize(), force) : y;
};

return force;
Expand Down
27 changes: 27 additions & 0 deletions test/center-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
var tape = require("tape"),
force = require("../");

require("./nodeEqual.js");

tape("forceCenter repositions nodes", function(test) {
const center = force.forceCenter(0, 0);
const f = force.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();
test.nodeEqual(a, { index: 0, x: -100, y: 0, vy: 0, vx: 0 });
test.nodeEqual(b, { index: 1, x: 0, y: 0, vy: 0, vx: 0 });
test.nodeEqual(c, { index: 2, x: 100, y: 0, vy: 0, vx: 0 });
test.end();
});


tape("forceCenter respects fixed positions", function(test) {
const center = force.forceCenter();
const f = force.forceSimulation().force("center", center).stop();
const a = { fx: 0, fy:0 }, b = {}, c = {};
f.nodes([a, b, c]);
f.tick();
test.nodeEqual(a, { fx: 0, fy: 0, index: 0, x: 0, y: 0, vy: 0, vx: 0 });
test.end();
});
48 changes: 48 additions & 0 deletions test/collide-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
var tape = require("tape"),
force = require("../");

require("./nodeEqual.js");

tape("forceCollide collides nodes", function(test) {
const collide = force.forceCollide(1);
const f = force.forceSimulation().force("collide", collide).stop();
const a = {}, b = {}, c = {};
f.nodes([a, b, c]);
f.tick(10);
test.nodeEqual(a, { index: 0, x: 7.0710678118654755, y: 0, vy: 0, vx: 0 });
test.nodeEqual(b, { index: 1, x: -9.03088751750192, y: 8.27303273571596, vy: 0, vx: 0 });
test.nodeEqual(c, { index: 2, x: 1.3823220809823638, y: -15.750847141167634, vy: 0, vx: 0 });
collide.radius(100);
f.tick(10);
test.nodeEqual(a, { index: 0, x: 174.08616723117228, y: 66.51743051995625, vy: 0.26976816231064354, vx: 0.677346615710878 });
test.nodeEqual(b, { index: 1, x: -139.73606544743998, y: 95.69860503079263, vy: 0.3545632444404687, vx: -0.5300880593105067 });
test.nodeEqual(c, { index: 2, x: -34.9275994083864, y: -169.69384995620052, vy: -0.6243314067511122, vx: -0.1472585564003713 });
test.end();
});


tape("forceCollide respects fixed positions", function(test) {
const collide = force.forceCollide(1);
const f = force.forceSimulation().force("collide", collide).stop();
const a = { fx: 0, fy:0 }, b = {}, c = {};
f.nodes([a, b, c]);
f.tick(10);
test.nodeEqual(a, { fx: 0, fy: 0, index: 0, x: 0, y: 0, vy: 0, vx: 0 });
collide.radius(100);
f.tick(10);
test.nodeEqual(a, { fx: 0, fy: 0, index: 0, x: 0, y: 0, vy: 0, vx: 0 });
test.end();
});

tape("forceCollide jiggles equal positions", function(test) {
const collide = force.forceCollide(1);
const f = force.forceSimulation().force("collide", collide).stop();
const a = { x: 0, y:0 }, b = { x:0, y: 0 };
f.nodes([a, b]);
f.tick();
test.assert(a.x !== b.x);
test.assert(a.y !== b.y);
test.equal(a.vx, -b.vx);
test.equal(a.vy, -b.vy);
test.end();
});
24 changes: 24 additions & 0 deletions test/find-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
var tape = require("tape"),
force = require("../");

require("./nodeEqual.js");

tape("simulation.find finds a node", function(test) {
const f = force.forceSimulation().stop();
const a = { x: 5, y: 0 }, b = { x: 10, y: 16 }, c = { x: -10, y: -4};
f.nodes([a, b, c]);
test.equal(f.find(0, 0), a);
test.equal(f.find(0, 20), b);
test.end();
});

tape("simulation.find(x, y, radius) finds a node within radius", function(test) {
const f = force.forceSimulation().stop();
const a = { x: 5, y: 0 }, b = { x: 10, y: 16 }, c = { x: -10, y: -4};
f.nodes([a, b, c]);
test.equal(f.find(0, 0), a);
test.equal(f.find(0, 0, 1), undefined);
test.equal(f.find(0, 20), b);
test.end();
});

23 changes: 23 additions & 0 deletions test/nodeEqual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
var tape = require("tape");

tape.Test.prototype.nodeEqual = nodeEqual;

function nodeEqual(actual, expected, delta) {
delta = delta || 1e-6;
this._assert(nodeEqual(actual, expected, delta), {
message: "should be similar",
operator: "nodeEqual",
actual: actual,
expected: expected
});

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);
}
}
21 changes: 21 additions & 0 deletions test/simulation-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
var tape = require("tape"),
force = require("../");

require("./nodeEqual.js");

tape("forceSimulation() returns a simulation", function(test) {
const f = force.forceSimulation().stop();
test.deepEqual(Object.keys(f).sort(), [ 'alpha', 'alphaDecay', 'alphaMin', 'alphaTarget', 'find', 'force', 'nodes', 'on', 'restart', 'stop', 'tick', 'velocityDecay' ]);
test.end();
});

tape("simulation.nodes(nodes) initializes a simulation with indices & phyllotaxis positions, 0 speed", function(test) {
const f = force.forceSimulation().stop();
const a = {}, b = {}, c = {};
f.nodes([a, b, c]);
test.nodeEqual(a, { index: 0, x: 7.0710678118654755, y: 0, vy: 0, vx: 0 });
test.nodeEqual(b, { index: 1, x: -9.03088751750192, y: 8.27303273571596, vy: 0, vx: 0 });
test.nodeEqual(c, { index: 2, x: 1.3823220809823638, y: -15.750847141167634, vy: 0, vx: 0 });
test.end();
});

84 changes: 84 additions & 0 deletions test/x-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
var tape = require("tape"),
force = require("../");

require("./nodeEqual.js");

tape("forceX centers nodes", function(test) {
const x = force.forceX(200);
const f = force.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);
test.assert(a.x > 190);
test.assert(a.vx > 0);
test.equal(b.x, 200);
test.equal(b.vx, 0);
test.assert(c.x < 210);
test.assert(c.vx < 0);
test.end();
});

tape("forceY centers nodes", function(test) {
const y = force.forceY(200);
const f = force.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);
test.assert(a.y > 190);
test.assert(a.vy > 0);
test.equal(b.y, 200);
test.equal(b.vy, 0);
test.assert(c.y < 210);
test.assert(c.vy < 0);
test.end();
});

tape("forceX respects fixed positions", function(test) {
const x = force.forceX(200);
const f = force.forceSimulation().force("x", x).stop();
const a = { fx: 0, fy:0 }, b = {}, c = {};
f.nodes([a, b, c]);
f.tick();
test.nodeEqual(a, { fx: 0, fy: 0, index: 0, x: 0, y: 0, vy: 0, vx: 0 });
test.end();
});

tape("forceY respects fixed positions", function(test) {
const y = force.forceX(200);
const f = force.forceSimulation().force("y", y).stop();
const a = { fx: 0, fy:0 }, b = {}, c = {};
f.nodes([a, b, c]);
f.tick();
test.nodeEqual(a, { fx: 0, fy: 0, index: 0, x: 0, y: 0, vy: 0, vx: 0 });
test.end();
});

tape("forceX.x() accessor", function(test) {
const x = force.forceX().x(d => d.x0);
const f = force.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);
test.assert(a.x > 290);
test.assert(a.vx > 0);
test.equal(b.x, 200);
test.equal(b.vx, 0);
test.assert(c.x < 110);
test.assert(c.vx < 0);
test.end();
});

tape("forceY.y() accessor", function(test) {
const y = force.forceY().y(d => d.y0);
const f = force.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);
test.assert(a.y > 290);
test.assert(a.vy > 0);
test.equal(b.y, 200);
test.equal(b.vy, 0);
test.assert(c.y < 110);
test.assert(c.vy < 0);
test.end();
});
Loading