Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prepare deps for d3v6 #171

Closed
wants to merge 12 commits into from
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