diff --git a/README.md b/README.md
index 616bca5..95fc0ef 100644
--- a/README.md
+++ b/README.md
@@ -40,11 +40,13 @@ var simulation = d3.forceSimulation(nodes);
# d3.forceSimulation([nodes]) · [Source](https://github.com/d3/d3-force/blob/master/src/simulation.js)
-Creates a new simulation with the specified array of [*nodes*](#simulation_nodes) and no [forces](#simulation_force). If *nodes* is not specified, it defaults to the empty array. The simulator [starts](#simulation_restart) automatically; use [*simulation*.on](#simulation_on) to listen for tick events as the simulation runs. If you wish to run the simulation manually instead, call [*simulation*.stop](#simulation_stop), and then call [*simulation*.tick](#simulation_tick) as desired.
+Creates a new simulation with the specified array of [*nodes*](#simulation_nodes) and no [forces](#simulation_force). If *nodes* is not specified, it defaults to the empty array. Use [*simulation*.on](#simulation_on) to listen for tick events as the simulation runs, and [*simulation*.start](#simulation_start) to start the simulator. If you wish to run the simulation manually instead, call [*simulation*.tick](#simulation_tick) as desired.
+
+# simulation.start() [<>](https://github.com/d3/d3-force/blob/master/src/simulation.js "Source")
# simulation.restart() · [Source](https://github.com/d3/d3-force/blob/master/src/simulation.js)
-Restarts the simulation’s internal timer and returns the simulation. In conjunction with [*simulation*.alphaTarget](#simulation_alphaTarget) or [*simulation*.alpha](#simulation_alpha), this method can be used to “reheat” the simulation during interaction, such as when dragging a node, or to resume the simulation after temporarily pausing it with [*simulation*.stop](#simulation_stop).
+Starts (or restarts) the simulation’s internal timer and returns the simulation. In conjunction with [*simulation*.alphaTarget](#simulation_alphaTarget) or [*simulation*.alpha](#simulation_alpha), this method can be used to “reheat” the simulation during interaction, such as when dragging a node, or to resume the simulation after temporarily pausing it with [*simulation*.stop](#simulation_stop).
# simulation.stop() · [Source](https://github.com/d3/d3-force/blob/master/src/simulation.js)
@@ -191,6 +193,10 @@ If *x* is specified, sets the *x*-coordinate of the centering position to the sp
If *y* is specified, sets the *y*-coordinate of the centering position to the specified number and returns this force. If *y* is not specified, returns the current *y*-coordinate, which defaults to zero.
+# center.strength([strength]) · [Source](https://github.com/d3/d3-force/blob/master/src/center.js)
+
+If *strength* is specified, sets the centering force’s strength. A reduced strength of e.g. 0.05 softens the movements on interactive graphs in which new nodes enter or exit the graph. If *strength* is not specified, returns the force’s current strength, which defaults to 1.
+
#### Collision
The collision force treats nodes as circles with a given [radius](#collide_radius), rather than points, and prevents nodes from overlapping. More formally, two nodes *a* and *b* are separated so that the distance between *a* and *b* is at least *radius*(*a*) + *radius*(*b*). To reduce jitter, this is by default a “soft” constraint with a configurable [strength](#collide_strength) and [iteration count](#collide_iterations).
@@ -425,11 +431,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.
-# d3.forceRadial(radius[, x][, y]) · [Source](https://github.com/d3/d3-force/blob/master/src/radial.js)
+# d3.forceRadial(radius[, x][, y][, angle]) · [Source](https://github.com/d3/d3-force/blob/master/src/radial.js)
[
](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.
# radial.strength([strength]) · [Source](https://github.com/d3/d3-force/blob/master/src/radial.js)
@@ -447,14 +453,37 @@ The strength accessor is invoked for each [node](#simulation_nodes) in the simul
# radial.radius([radius]) · [Source](https://github.com/d3/d3-force/blob/master/src/radial.js)
-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.
# radial.x([x]) · [Source](https://github.com/d3/d3-force/blob/master/src/radial.js)
-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.
# radial.y([y]) · [Source](https://github.com/d3/d3-force/blob/master/src/radial.js)
-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.
+
+# radial.angle([angle]) [<>](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.
+
diff --git a/package.json b/package.json
index 8f8a8e2..062929b 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,6 @@
{
"name": "d3-force",
- "version": "2.1.0-rc.2",
- "publishConfig": {
- "tag": "next"
- },
+ "version": "2.0.1",
"description": "Force-directed graph layout using velocity Verlet integration.",
"keywords": [
"d3",
diff --git a/src/center.js b/src/center.js
index b8ce38e..cf1baef 100644
--- a/src/center.js
+++ b/src/center.js
@@ -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;
@@ -15,8 +15,11 @@ 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) * strength;
+ sy = (sy / n - y) * strength;
+
+ for (i = 0; i < n; ++i) {
+ node = nodes[i], node.vx -= sx, node.vy -= sy;
}
}
@@ -32,5 +35,9 @@ export default function(x, y) {
return arguments.length ? (y = +_, force) : y;
};
+ force.strength = function(_) {
+ return arguments.length ? (strength = +_, force) : strength;
+ };
+
return force;
}
diff --git a/src/math.js b/src/math.js
new file mode 100644
index 0000000..d38afca
--- /dev/null
+++ b/src/math.js
@@ -0,0 +1,2 @@
+export var pi = Math.PI;
+export var radians = pi / 180;
diff --git a/src/radial.js b/src/radial.js
index 609516b..0f59d9e 100644
--- a/src/radial.js
+++ b/src/radial.js
@@ -1,24 +1,52 @@
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);
+ }
}
}
@@ -26,10 +54,16 @@ export default function(radius, x, y) {
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);
}
}
@@ -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;
diff --git a/src/simulation.js b/src/simulation.js
index f1e751d..bb2f9b5 100644
--- a/src/simulation.js
+++ b/src/simulation.js
@@ -20,8 +20,9 @@ export default function(nodes) {
alphaTarget = 0,
velocityDecay = 0.6,
forces = new Map(),
- stepper = timer(step),
- event = dispatch("tick", "end");
+ event = dispatch("tick", "end"),
+ stepper = timer(step);
+ stepper.stop();
if (nodes == null) nodes = [];
@@ -84,6 +85,10 @@ export default function(nodes) {
return simulation = {
tick: tick,
+ start: function() {
+ return this.restart();
+ },
+
restart: function() {
return stepper.restart(step), simulation;
},
@@ -144,7 +149,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, _), simulation)
+ : event.on(name);
}
};
}
diff --git a/test/center-test.js b/test/center-test.js
new file mode 100644
index 0000000..6b468c8
--- /dev/null
+++ b/test/center-test.js
@@ -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();
+});
diff --git a/test/collide-test.js b/test/collide-test.js
new file mode 100644
index 0000000..f9ed20f
--- /dev/null
+++ b/test/collide-test.js
@@ -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();
+});
diff --git a/test/find-test.js b/test/find-test.js
new file mode 100644
index 0000000..e7c0dd2
--- /dev/null
+++ b/test/find-test.js
@@ -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();
+});
+
diff --git a/test/nodeEqual.js b/test/nodeEqual.js
new file mode 100644
index 0000000..788f02e
--- /dev/null
+++ b/test/nodeEqual.js
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/test/simulation-test.js b/test/simulation-test.js
new file mode 100644
index 0000000..ecb0dc0
--- /dev/null
+++ b/test/simulation-test.js
@@ -0,0 +1,36 @@
+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', 'start', '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();
+});
+
+tape("simulation.nodes(nodes).start() starts the simulator", function(test) {
+ const f = force.forceSimulation();
+ const a = {}, b = {}, c = {};
+ f.nodes([a, b, c]);
+ setTimeout(() => {
+ test.equal(f.alpha(), 1);
+ f.start();
+ setTimeout(() => {
+ test.assert(f.alpha() < 1);
+ f.stop();
+ test.end();
+ }, 100);
+ }, 100);
+});
+
diff --git a/test/x-test.js b/test/x-test.js
new file mode 100644
index 0000000..51a989f
--- /dev/null
+++ b/test/x-test.js
@@ -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();
+});