463 lines
12 KiB
JavaScript
Executable file
463 lines
12 KiB
JavaScript
Executable file
/* eslint-disable no-shadow */
|
|
var test = require('tap').test,
|
|
createGraph = require('ngraph.graph'),
|
|
createLayout = require('..');
|
|
|
|
test('it exposes simulator', function(t) {
|
|
t.ok(typeof createLayout.simulator === 'function', 'Simulator is exposed');
|
|
t.end();
|
|
});
|
|
|
|
test('it returns spring', function(t) {
|
|
var g = createGraph();
|
|
var layout = createLayout(g);
|
|
|
|
var link = g.addLink(1, 2);
|
|
|
|
var springForLink = layout.getSpring(link);
|
|
var springForLinkId = layout.getSpring(link.id);
|
|
var springForFromTo = layout.getSpring(1, 2);
|
|
|
|
t.ok(springForLink, 'spring is here');
|
|
t.ok(springForLinkId === springForLink, 'Spring is the same');
|
|
t.ok(springForFromTo === springForLink, 'Spring is the same');
|
|
t.end();
|
|
});
|
|
|
|
test('it returns same position', function(t) {
|
|
var g = createGraph();
|
|
var layout = createLayout(g);
|
|
|
|
g.addLink(1, 2);
|
|
|
|
var firstNodePos = layout.getNodePosition(1);
|
|
layout.step();
|
|
t.ok(firstNodePos === layout.getNodePosition(1), 'Position is the same object');
|
|
layout.step();
|
|
t.ok(firstNodePos === layout.getNodePosition(1), 'Position is the same object after multiple steps');
|
|
t.end();
|
|
});
|
|
|
|
test('it returns body', function(t) {
|
|
var g = createGraph();
|
|
var layout = createLayout(g);
|
|
|
|
g.addLink(1, 2);
|
|
|
|
t.ok(layout.getBody(1), 'node 1 has body');
|
|
t.ok(layout.getBody(2), 'node 2 has body');
|
|
t.notOk(layout.getBody(4), 'there is no node 4');
|
|
|
|
var body = layout.getBody(1);
|
|
t.ok(body.pos.x && body.pos.y, 'Body has a position');
|
|
t.ok(body.mass, 'Body has a mass');
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('it can set node mass', function(t) {
|
|
var g = createGraph();
|
|
g.addNode('anvaka');
|
|
|
|
var layout = createLayout(g, {
|
|
nodeMass: function (nodeId) {
|
|
t.equal(nodeId, 'anvaka', 'correct node is called');
|
|
return 84; // my mass in kilograms :P
|
|
}
|
|
});
|
|
|
|
var body = layout.getBody('anvaka');
|
|
t.equal(body.mass, 84, 'Mass is okay');
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('does not tolerate bad input', function (t) {
|
|
t.throws(missingGraph);
|
|
t.throws(invalidNodeId);
|
|
t.end();
|
|
|
|
function missingGraph() {
|
|
// graph is missing:
|
|
createLayout();
|
|
}
|
|
|
|
function invalidNodeId() {
|
|
var graph = createGraph();
|
|
var layout = createLayout(graph);
|
|
|
|
// we don't have nodes in the graph. This should throw:
|
|
layout.getNodePosition(1);
|
|
}
|
|
});
|
|
|
|
test('it fires stable on empty graph', function(t) {
|
|
var graph = createGraph();
|
|
var layout = createLayout(graph);
|
|
layout.on('stable', endTest);
|
|
layout.step();
|
|
|
|
function endTest() {
|
|
t.end();
|
|
}
|
|
});
|
|
|
|
test('can add bodies which are standard prototype names', function (t) {
|
|
var graph = createGraph();
|
|
graph.addLink('constructor', 'watch');
|
|
|
|
var layout = createLayout(graph);
|
|
layout.step();
|
|
|
|
graph.forEachNode(function (node) {
|
|
var pos = layout.getNodePosition(node.id);
|
|
t.ok(pos && typeof pos.x === 'number' &&
|
|
typeof pos.y === 'number', 'Position is defined');
|
|
});
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('it can step when no links present', function (t) {
|
|
var graph = createGraph();
|
|
graph.addNode('constructor');
|
|
graph.addNode('watch');
|
|
|
|
var layout = createLayout(graph);
|
|
layout.step();
|
|
|
|
graph.forEachNode(function (node) {
|
|
var pos = layout.getNodePosition(node.id);
|
|
t.ok(pos && typeof pos.x === 'number' &&
|
|
typeof pos.y === 'number', 'Position is defined');
|
|
});
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('layout initializes nodes positions', function (t) {
|
|
var graph = createGraph();
|
|
graph.addLink(1, 2);
|
|
|
|
var layout = createLayout(graph);
|
|
|
|
// perform one iteration of layout:
|
|
layout.step();
|
|
|
|
graph.forEachNode(function (node) {
|
|
var pos = layout.getNodePosition(node.id);
|
|
t.ok(pos && typeof pos.x === 'number' &&
|
|
typeof pos.y === 'number', 'Position is defined');
|
|
});
|
|
|
|
graph.forEachLink(function (link) {
|
|
var linkPos = layout.getLinkPosition(link.id);
|
|
t.ok(linkPos && linkPos.from && linkPos.to, 'Link position is defined');
|
|
var fromPos = layout.getNodePosition(link.fromId);
|
|
t.ok(linkPos.from === fromPos, '"From" should be identical to getNodePosition');
|
|
var toPos = layout.getNodePosition(link.toId);
|
|
t.ok(linkPos.to === toPos, '"To" should be identical to getNodePosition');
|
|
});
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('Layout can set node position', function (t) {
|
|
var graph = createGraph();
|
|
graph.addLink(1, 2);
|
|
|
|
var layout = createLayout(graph);
|
|
|
|
layout.pinNode(graph.getNode(1), true);
|
|
layout.setNodePosition(1, 42, 42);
|
|
|
|
// perform one iteration of layout:
|
|
layout.step();
|
|
|
|
// and make sure node 1 was not moved:
|
|
var actualPosition = layout.getNodePosition(1);
|
|
t.equal(actualPosition.x, 42, 'X has not changed');
|
|
t.equal(actualPosition.y, 42, 'Y has not changed');
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('Layout updates bounding box when it sets node position', function (t) {
|
|
var graph = createGraph();
|
|
graph.addLink(1, 2);
|
|
|
|
var layout = createLayout(graph);
|
|
layout.setNodePosition(1, 42, 42);
|
|
layout.setNodePosition(2, 40, 40);
|
|
var rect = layout.getGraphRect();
|
|
t.ok(rect.max_x <= 42); t.ok(rect.max_y <= 42);
|
|
t.ok(rect.min_x >= 40); t.ok(rect.min_y >= 40);
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('layout initializes links', function (t) {
|
|
var graph = createGraph();
|
|
var node1 = graph.addNode(1); node1.position = {x : -1000, y: 0};
|
|
var node2 = graph.addNode(2); node2.position = {x : 1000, y: 0};
|
|
|
|
graph.addLink(1, 2);
|
|
|
|
var layout = createLayout(graph);
|
|
|
|
// perform one iteration of layout:
|
|
layout.step();
|
|
|
|
// since both nodes are connected by spring and distance is too large between
|
|
// them, they should start attracting each other
|
|
var pos1 = layout.getNodePosition(1);
|
|
var pos2 = layout.getNodePosition(2);
|
|
|
|
t.ok(pos1.x > -1000, 'Node 1 moves towards node 2');
|
|
t.ok(pos2.x < 1000, 'Node 1 moves towards node 2');
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('layout respects proposed original position', function (t) {
|
|
var graph = createGraph();
|
|
var node = graph.addNode(1);
|
|
|
|
var initialPosition = {x: 100, y: 100};
|
|
node.position = copy(initialPosition);
|
|
|
|
var layout = createLayout(graph);
|
|
layout.step();
|
|
|
|
t.same(layout.getNodePosition(node.id), initialPosition, 'original position preserved');
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('layout has defined graph rectangle', function (t) {
|
|
t.test('empty graph', function (t) {
|
|
var graph = createGraph();
|
|
var layout = createLayout(graph);
|
|
|
|
var rect = layout.getGraphRect();
|
|
var expectedProperties = ['min_x', 'min_y', 'max_x', 'max_y'];
|
|
t.ok(rect && expectedProperties.reduce(hasProperties, true), 'Values are present before step()');
|
|
|
|
layout.step();
|
|
|
|
t.ok(rect && expectedProperties.reduce(hasProperties, true), 'Values are present after step()');
|
|
t.end();
|
|
|
|
function hasProperties(result, key) {
|
|
return result && typeof rect[key] === 'number';
|
|
}
|
|
});
|
|
|
|
t.test('two nodes', function (t) {
|
|
var graph = createGraph();
|
|
graph.addLink(1, 2);
|
|
var layout = createLayout(graph);
|
|
layout.step();
|
|
|
|
var rect = layout.getGraphRect();
|
|
t.ok(!rectangleIsEmpty(rect), 'Graph rectangle is not empty');
|
|
|
|
t.end();
|
|
});
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('it does not move pinned nodes', function (t) {
|
|
t.test('respects original data.isPinned attribute', function (t) {
|
|
var graph = createGraph();
|
|
var testNode = graph.addNode(1, { isPinned: true });
|
|
var layout = createLayout(graph);
|
|
t.ok(layout.isNodePinned(testNode), 'Node is pinned');
|
|
t.end();
|
|
});
|
|
|
|
t.test('respects node.isPinned attribute', function (t) {
|
|
var graph = createGraph();
|
|
var testNode = graph.addNode(1);
|
|
|
|
// this was possible in vivagraph. Port it over to ngraph:
|
|
testNode.isPinned = true;
|
|
var layout = createLayout(graph);
|
|
t.ok(layout.isNodePinned(testNode), 'Node is pinned');
|
|
t.end();
|
|
});
|
|
|
|
t.test('can pin nodes after graph is initialized', function (t) {
|
|
var graph = createGraph();
|
|
graph.addLink(1, 2);
|
|
|
|
var layout = createLayout(graph);
|
|
layout.pinNode(graph.getNode(1), true);
|
|
layout.step();
|
|
var pos1 = copy(layout.getNodePosition(1));
|
|
var pos2 = copy(layout.getNodePosition(2));
|
|
|
|
// make one more step and make sure node 1 did not move:
|
|
layout.step();
|
|
|
|
t.ok(!positionChanged(pos1, layout.getNodePosition(1)), 'Node 1 was not moved');
|
|
t.ok(positionChanged(pos2, layout.getNodePosition(2)), 'Node 2 has moved');
|
|
|
|
t.end();
|
|
});
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('it listens to graph events', function (t) {
|
|
// we first initialize with empty graph:
|
|
var graph = createGraph();
|
|
var layout = createLayout(graph);
|
|
|
|
// and only then add nodes:
|
|
graph.addLink(1, 2);
|
|
|
|
// make two iterations
|
|
layout.step();
|
|
var pos1 = copy(layout.getNodePosition(1));
|
|
var pos2 = copy(layout.getNodePosition(2));
|
|
|
|
layout.step();
|
|
|
|
t.ok(positionChanged(pos1, layout.getNodePosition(1)), 'Node 1 has moved');
|
|
t.ok(positionChanged(pos2, layout.getNodePosition(2)), 'Node 2 has moved');
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('can stop listen to events', function (t) {
|
|
// we first initialize with empty graph:
|
|
var graph = createGraph();
|
|
var layout = createLayout(graph);
|
|
layout.dispose();
|
|
|
|
graph.addLink(1, 2);
|
|
layout.step();
|
|
t.ok(layout.simulator.bodies.length === 0, 'No bodies in the simulator');
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('physics simulator', function (t) {
|
|
t.test('has default simulator', function (t) {
|
|
var graph = createGraph();
|
|
var layout = createLayout(graph);
|
|
|
|
t.ok(layout.simulator, 'physics simulator is present');
|
|
t.end();
|
|
});
|
|
|
|
t.test('can override default settings', function (t) {
|
|
var graph = createGraph();
|
|
var layout = createLayout(graph, {
|
|
theta: 1.5
|
|
});
|
|
t.equal(layout.simulator.theta(), 1.5, 'Simulator settings are overridden');
|
|
t.end();
|
|
});
|
|
|
|
t.end();
|
|
});
|
|
|
|
test('it removes removed nodes', function (t) {
|
|
var graph = createGraph();
|
|
var layout = createLayout(graph);
|
|
graph.addLink(1, 2);
|
|
|
|
layout.step();
|
|
graph.clear();
|
|
|
|
// since we removed everything from graph rect should be empty:
|
|
var rect = layout.getGraphRect();
|
|
|
|
t.ok(rectangleIsEmpty(rect), 'Graph rect is empty');
|
|
t.end();
|
|
});
|
|
|
|
test('it can iterate over bodies', function(t) {
|
|
var graph = createGraph();
|
|
var layout = createLayout(graph);
|
|
graph.addLink(1, 2);
|
|
var calledCount = 0;
|
|
|
|
layout.forEachBody(function(body, bodyId) {
|
|
t.ok(body.pos, bodyId + ' has position');
|
|
t.ok(graph.getNode(bodyId), bodyId + ' matches a graph node');
|
|
calledCount += 1;
|
|
});
|
|
|
|
t.equal(calledCount, 2, 'Both bodies are visited');
|
|
t.end();
|
|
});
|
|
|
|
test('it handles large graphs', function (t) {
|
|
var graph = createGraph();
|
|
var layout = createLayout(graph);
|
|
|
|
var count = 60000;
|
|
|
|
var i = count;
|
|
while (i--) {
|
|
graph.addNode(i);
|
|
}
|
|
|
|
// link each node to 2 other random nodes
|
|
i = count;
|
|
while (i--) {
|
|
graph.addLink(i, Math.ceil(Math.random() * count));
|
|
graph.addLink(i, Math.ceil(Math.random() * count));
|
|
}
|
|
|
|
layout.step();
|
|
|
|
t.ok(layout.simulator.bodies.length !== 0, 'Bodies in the simulator');
|
|
t.end();
|
|
});
|
|
|
|
test('it can create high dimensional layout', function(t) {
|
|
var graph = createGraph();
|
|
graph.addLink(1, 2);
|
|
var layout = createLayout(graph, {dimensions: 6});
|
|
layout.step();
|
|
|
|
var pos = layout.getNodePosition(1);
|
|
t.ok(pos.x !== undefined, 'Position has x');
|
|
t.ok(pos.y !== undefined, 'Position has y');
|
|
t.ok(pos.z !== undefined, 'Position has z');
|
|
t.ok(pos.c4 !== undefined, 'Position has c4');
|
|
t.ok(pos.c5 !== undefined, 'Position has c5');
|
|
t.ok(pos.c6 !== undefined, 'Position has c6');
|
|
t.end();
|
|
});
|
|
|
|
test('it can layout two graphs independently', function(t) {
|
|
var graph1 = createGraph();
|
|
var graph2 = createGraph();
|
|
var layout1 = createLayout(graph1);
|
|
var layout2 = createLayout(graph2);
|
|
graph1.addLink(1, 2);
|
|
graph2.addLink(1, 2);
|
|
layout1.step();
|
|
layout2.step();
|
|
layout2.step();
|
|
t.ok(layout1.getNodePosition(1).x !== layout2.getNodePosition(1).x, 'Positions are different');
|
|
t.end();
|
|
});
|
|
|
|
function positionChanged(pos1, pos2) {
|
|
return (pos1.x !== pos2.x) || (pos1.y !== pos2.y);
|
|
}
|
|
|
|
function copy(obj) {
|
|
return JSON.parse(JSON.stringify(obj));
|
|
}
|
|
|
|
function rectangleIsEmpty(rect) {
|
|
return rect.min_x === 0 && rect.min_y === 0 && rect.max_x === 0 && rect.max_y === 0;
|
|
}
|