Skip to content
Closed
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,11 +425,11 @@ function y() {

The *y*-accessor is invoked for each [node](#simulation_nodes) in the simulation, being passed the *node* and its zero-based *index*. The resulting number is then stored internally, such that the target *y*-coordinate of each node is only recomputed when the force is initialized or when this method is called with a new *y*, and not on every application of the force.

<a name="forceRadial" href="#forceRadial">#</a> d3.<b>forceRadial</b>(<i>radius</i>[, <i>x</i>][, <i>y</i>]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source")
<a name="forceRadial" href="#forceRadial">#</a> d3.<b>forceRadial</b>(<i>radius</i>[, <i>x</i>][, <i>y</i>][, <i>angle</i>]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source")

[<img alt="Radial Force" src="https://raw.githubusercontent.com/d3/d3-force/master/img/radial.png" width="420" height="219">](https://bl.ocks.org/mbostock/cd98bf52e9067e26945edd95e8cf6ef9)

Creates a new positioning force towards a circle of the specified [*radius*](#radial_radius) centered at ⟨[*x*](#radial_x),[*y*](#radial_y)⟩. If *x* and *y* are not specified, they default to ⟨0,0⟩.
Creates a new positioning force towards a circle of the specified [*radius*](#radial_radius) centered at ⟨[*x*](#radial_x),[*y*](#radial_y)⟩, and with a preferred [*angle*](#radial_angle). If *x* and *y* are not specified, they default to ⟨0,0⟩. If *radius* or *angle* are not specified (or null), they are ignored.

<a name="radial_strength" href="#radial_strength">#</a> <i>radial</i>.<b>strength</b>([<i>strength</i>]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source")

Expand All @@ -447,14 +447,37 @@ The strength accessor is invoked for each [node](#simulation_nodes) in the simul

<a name="radial_radius" href="#radial_radius">#</a> <i>radial</i>.<b>radius</b>([<i>radius</i>]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source")

If *radius* is specified, sets the circle *radius* to the specified number or function, re-evaluates the *radius* accessor for each node, and returns this force. If *radius* is not specified, returns the current *radius* accessor.
If *radius* is specified, sets the circle *radius* to the specified number or function, re-evaluates the *radius* accessor for each node, and returns this force. If *radius* is not specified, returns the current *radius* accessor. If *angle* is null, the force ignores the radius (see [*radial*.angle](#radial_angle)).

The *radius* accessor is invoked for each [node](#simulation_nodes) in the simulation, being passed the *node* and its zero-based *index*. The resulting number is then stored internally, such that the target radius of each node is only recomputed when the force is initialized or when this method is called with a new *radius*, and not on every application of the force.

<a name="radial_x" href="#radial_x">#</a> <i>radial</i>.<b>x</b>([<i>x</i>]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source")

If *x* is specified, sets the *x*-coordinate of the circle center to the specified number and returns this force. If *x* is not specified, returns the current *x*-coordinate of the center, which defaults to zero.
If *x* is specified, sets the *x*-coordinate accessor to the specified number or function, re-evaluates the *x*-accessor for each node, and returns this force. If *x* is not specified, returns the current *x*-accessor, which defaults to:

```js
function x() {
return 0;
}
```

The *x*-accessor is invoked for each [node](#simulation_nodes) in the simulation, being passed the *node* and its zero-based *index*. The resulting number is then stored internally, such that the target *x*-coordinate of each node is only recomputed when the force is initialized or when this method is called with a new *x*, and not on every application of the force.

<a name="radial_y" href="#radial_y">#</a> <i>radial</i>.<b>y</b>([<i>y</i>]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source")

If *y* is specified, sets the *y*-coordinate of the circle center to the specified number and returns this force. If *y* is not specified, returns the current *y*-coordinate of the center, which defaults to zero.
If *y* is specified, sets the *y*-coordinate accessor to the specified number or function, re-evaluates the *y*-accessor for each node, and returns this force. If *y* is not specified, returns the current *y*-accessor, which defaults to:

```js
function y() {
return 0;
}
```

The *y*-accessor is invoked for each [node](#simulation_nodes) in the simulation, being passed the *node* and its zero-based *index*. The resulting number is then stored internally, such that the target *y*-coordinate of each node is only recomputed when the force is initialized or when this method is called with a new *y*, and not on every application of the force.

<a name="radial_angle" href="#radial_angle">#</a> <i>radial</i>.<b>angle</b>([<i>angle</i>]) [<>](https://github.com/d3/d3-force/blob/master/src/radial.js "Source")

If *angle* is specified, sets the preferred *angle* to the specified number or function, re-evaluates the *angle* accessor for each node, and returns this force. If *angle* is not specified, returns the current *angle* accessor. If *angle* is null, the force ignores the preferred angle.

The *angle* accessor is invoked for each [node](#simulation_nodes) in the simulation, being passed the *node* and its zero-based *index*. The resulting number is then stored internally, such that the target angle of each node is only recomputed when the force is initialized or when this method is called with a new *angle*, and not on every application of the force.

11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"name": "d3-force",
"version": "2.0.1",
"version": "2.1.0-rc.1",
"publishConfig": {
"tag": "next"
},
"description": "Force-directed graph layout using velocity Verlet integration.",
"keywords": [
"d3",
Expand Down Expand Up @@ -37,9 +40,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
10 changes: 6 additions & 4 deletions src/center.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export default function(x, y) {
var nodes;
var nodes, strength = 0.05;

if (x == null) x = 0;
if (y == null) y = 0;

function force() {
function force(alpha) {
var i,
n = nodes.length,
node,
Expand All @@ -15,8 +15,10 @@ 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) {
node = nodes[i], node.x -= sx, node.y -= sy;
sx = (sx / n - x) * alpha * strength;
sy = (sy / n - y) * alpha * strength;
for (i = 0; i < n; ++i) {
node = nodes[i], node.vx -= sx, node.vy -= sy;
}
}

Expand Down
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
5 changes: 4 additions & 1 deletion src/simulation.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default function(nodes) {
velocityDecay = 0.6,
forces = new Map(),
stepper = timer(step),
started = stepper.stop() || 0,
event = dispatch("tick", "end");

if (nodes == null) nodes = [];
Expand Down Expand Up @@ -144,7 +145,9 @@ export default function(nodes) {
},

on: function(name, _) {
return arguments.length > 1 ? (event.on(name, _), simulation) : event.on(name);
return arguments.length > 1
? (event.on(name, _), started++ || stepper.restart(step), simulation)
: event.on(name);
}
};
}
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.alphaDecay(0).tick(250);
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();
});

Loading