layout.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. /* eslint-disable no-shadow */
  2. var test = require('tap').test,
  3. createGraph = require('ngraph.graph'),
  4. createLayout = require('..');
  5. test('it exposes simulator', function(t) {
  6. t.ok(typeof createLayout.simulator === 'function', 'Simulator is exposed');
  7. t.end();
  8. });
  9. test('it returns spring', function(t) {
  10. var g = createGraph();
  11. var layout = createLayout(g);
  12. var link = g.addLink(1, 2);
  13. var springForLink = layout.getSpring(link);
  14. var springForLinkId = layout.getSpring(link.id);
  15. var springForFromTo = layout.getSpring(1, 2);
  16. t.ok(springForLink, 'spring is here');
  17. t.ok(springForLinkId === springForLink, 'Spring is the same');
  18. t.ok(springForFromTo === springForLink, 'Spring is the same');
  19. t.end();
  20. });
  21. test('it returns same position', function(t) {
  22. var g = createGraph();
  23. var layout = createLayout(g);
  24. g.addLink(1, 2);
  25. var firstNodePos = layout.getNodePosition(1);
  26. layout.step();
  27. t.ok(firstNodePos === layout.getNodePosition(1), 'Position is the same object');
  28. layout.step();
  29. t.ok(firstNodePos === layout.getNodePosition(1), 'Position is the same object after multiple steps');
  30. t.end();
  31. });
  32. test('it returns body', function(t) {
  33. var g = createGraph();
  34. var layout = createLayout(g);
  35. g.addLink(1, 2);
  36. t.ok(layout.getBody(1), 'node 1 has body');
  37. t.ok(layout.getBody(2), 'node 2 has body');
  38. t.notOk(layout.getBody(4), 'there is no node 4');
  39. var body = layout.getBody(1);
  40. t.ok(body.pos.x && body.pos.y, 'Body has a position');
  41. t.ok(body.mass, 'Body has a mass');
  42. t.end();
  43. });
  44. test('it can set node mass', function(t) {
  45. var g = createGraph();
  46. g.addNode('anvaka');
  47. var layout = createLayout(g, {
  48. nodeMass: function (nodeId) {
  49. t.equal(nodeId, 'anvaka', 'correct node is called');
  50. return 84; // my mass in kilograms :P
  51. }
  52. });
  53. var body = layout.getBody('anvaka');
  54. t.equal(body.mass, 84, 'Mass is okay');
  55. t.end();
  56. });
  57. test('does not tolerate bad input', function (t) {
  58. t.throws(missingGraph);
  59. t.throws(invalidNodeId);
  60. t.end();
  61. function missingGraph() {
  62. // graph is missing:
  63. createLayout();
  64. }
  65. function invalidNodeId() {
  66. var graph = createGraph();
  67. var layout = createLayout(graph);
  68. // we don't have nodes in the graph. This should throw:
  69. layout.getNodePosition(1);
  70. }
  71. });
  72. test('it fires stable on empty graph', function(t) {
  73. var graph = createGraph();
  74. var layout = createLayout(graph);
  75. layout.on('stable', endTest);
  76. layout.step();
  77. function endTest() {
  78. t.end();
  79. }
  80. });
  81. test('can add bodies which are standard prototype names', function (t) {
  82. var graph = createGraph();
  83. graph.addLink('constructor', 'watch');
  84. var layout = createLayout(graph);
  85. layout.step();
  86. graph.forEachNode(function (node) {
  87. var pos = layout.getNodePosition(node.id);
  88. t.ok(pos && typeof pos.x === 'number' &&
  89. typeof pos.y === 'number', 'Position is defined');
  90. });
  91. t.end();
  92. });
  93. test('it can step when no links present', function (t) {
  94. var graph = createGraph();
  95. graph.addNode('constructor');
  96. graph.addNode('watch');
  97. var layout = createLayout(graph);
  98. layout.step();
  99. graph.forEachNode(function (node) {
  100. var pos = layout.getNodePosition(node.id);
  101. t.ok(pos && typeof pos.x === 'number' &&
  102. typeof pos.y === 'number', 'Position is defined');
  103. });
  104. t.end();
  105. });
  106. test('layout initializes nodes positions', function (t) {
  107. var graph = createGraph();
  108. graph.addLink(1, 2);
  109. var layout = createLayout(graph);
  110. // perform one iteration of layout:
  111. layout.step();
  112. graph.forEachNode(function (node) {
  113. var pos = layout.getNodePosition(node.id);
  114. t.ok(pos && typeof pos.x === 'number' &&
  115. typeof pos.y === 'number', 'Position is defined');
  116. });
  117. graph.forEachLink(function (link) {
  118. var linkPos = layout.getLinkPosition(link.id);
  119. t.ok(linkPos && linkPos.from && linkPos.to, 'Link position is defined');
  120. var fromPos = layout.getNodePosition(link.fromId);
  121. t.ok(linkPos.from === fromPos, '"From" should be identical to getNodePosition');
  122. var toPos = layout.getNodePosition(link.toId);
  123. t.ok(linkPos.to === toPos, '"To" should be identical to getNodePosition');
  124. });
  125. t.end();
  126. });
  127. test('Layout can set node position', function (t) {
  128. var graph = createGraph();
  129. graph.addLink(1, 2);
  130. var layout = createLayout(graph);
  131. layout.pinNode(graph.getNode(1), true);
  132. layout.setNodePosition(1, 42, 42);
  133. // perform one iteration of layout:
  134. layout.step();
  135. // and make sure node 1 was not moved:
  136. var actualPosition = layout.getNodePosition(1);
  137. t.equal(actualPosition.x, 42, 'X has not changed');
  138. t.equal(actualPosition.y, 42, 'Y has not changed');
  139. t.end();
  140. });
  141. test('Layout updates bounding box when it sets node position', function (t) {
  142. var graph = createGraph();
  143. graph.addLink(1, 2);
  144. var layout = createLayout(graph);
  145. layout.setNodePosition(1, 42, 42);
  146. layout.setNodePosition(2, 40, 40);
  147. var rect = layout.getGraphRect();
  148. t.ok(rect.max_x <= 42); t.ok(rect.max_y <= 42);
  149. t.ok(rect.min_x >= 40); t.ok(rect.min_y >= 40);
  150. t.end();
  151. });
  152. test('layout initializes links', function (t) {
  153. var graph = createGraph();
  154. var node1 = graph.addNode(1); node1.position = {x : -1000, y: 0};
  155. var node2 = graph.addNode(2); node2.position = {x : 1000, y: 0};
  156. graph.addLink(1, 2);
  157. var layout = createLayout(graph);
  158. // perform one iteration of layout:
  159. layout.step();
  160. // since both nodes are connected by spring and distance is too large between
  161. // them, they should start attracting each other
  162. var pos1 = layout.getNodePosition(1);
  163. var pos2 = layout.getNodePosition(2);
  164. t.ok(pos1.x > -1000, 'Node 1 moves towards node 2');
  165. t.ok(pos2.x < 1000, 'Node 1 moves towards node 2');
  166. t.end();
  167. });
  168. test('layout respects proposed original position', function (t) {
  169. var graph = createGraph();
  170. var node = graph.addNode(1);
  171. var initialPosition = {x: 100, y: 100};
  172. node.position = copy(initialPosition);
  173. var layout = createLayout(graph);
  174. layout.step();
  175. t.same(layout.getNodePosition(node.id), initialPosition, 'original position preserved');
  176. t.end();
  177. });
  178. test('layout has defined graph rectangle', function (t) {
  179. t.test('empty graph', function (t) {
  180. var graph = createGraph();
  181. var layout = createLayout(graph);
  182. var rect = layout.getGraphRect();
  183. var expectedProperties = ['min_x', 'min_y', 'max_x', 'max_y'];
  184. t.ok(rect && expectedProperties.reduce(hasProperties, true), 'Values are present before step()');
  185. layout.step();
  186. t.ok(rect && expectedProperties.reduce(hasProperties, true), 'Values are present after step()');
  187. t.end();
  188. function hasProperties(result, key) {
  189. return result && typeof rect[key] === 'number';
  190. }
  191. });
  192. t.test('two nodes', function (t) {
  193. var graph = createGraph();
  194. graph.addLink(1, 2);
  195. var layout = createLayout(graph);
  196. layout.step();
  197. var rect = layout.getGraphRect();
  198. t.ok(!rectangleIsEmpty(rect), 'Graph rectangle is not empty');
  199. t.end();
  200. });
  201. t.end();
  202. });
  203. test('it does not move pinned nodes', function (t) {
  204. t.test('respects original data.isPinned attribute', function (t) {
  205. var graph = createGraph();
  206. var testNode = graph.addNode(1, { isPinned: true });
  207. var layout = createLayout(graph);
  208. t.ok(layout.isNodePinned(testNode), 'Node is pinned');
  209. t.end();
  210. });
  211. t.test('respects node.isPinned attribute', function (t) {
  212. var graph = createGraph();
  213. var testNode = graph.addNode(1);
  214. // this was possible in vivagraph. Port it over to ngraph:
  215. testNode.isPinned = true;
  216. var layout = createLayout(graph);
  217. t.ok(layout.isNodePinned(testNode), 'Node is pinned');
  218. t.end();
  219. });
  220. t.test('can pin nodes after graph is initialized', function (t) {
  221. var graph = createGraph();
  222. graph.addLink(1, 2);
  223. var layout = createLayout(graph);
  224. layout.pinNode(graph.getNode(1), true);
  225. layout.step();
  226. var pos1 = copy(layout.getNodePosition(1));
  227. var pos2 = copy(layout.getNodePosition(2));
  228. // make one more step and make sure node 1 did not move:
  229. layout.step();
  230. t.ok(!positionChanged(pos1, layout.getNodePosition(1)), 'Node 1 was not moved');
  231. t.ok(positionChanged(pos2, layout.getNodePosition(2)), 'Node 2 has moved');
  232. t.end();
  233. });
  234. t.end();
  235. });
  236. test('it listens to graph events', function (t) {
  237. // we first initialize with empty graph:
  238. var graph = createGraph();
  239. var layout = createLayout(graph);
  240. // and only then add nodes:
  241. graph.addLink(1, 2);
  242. // make two iterations
  243. layout.step();
  244. var pos1 = copy(layout.getNodePosition(1));
  245. var pos2 = copy(layout.getNodePosition(2));
  246. layout.step();
  247. t.ok(positionChanged(pos1, layout.getNodePosition(1)), 'Node 1 has moved');
  248. t.ok(positionChanged(pos2, layout.getNodePosition(2)), 'Node 2 has moved');
  249. t.end();
  250. });
  251. test('can stop listen to events', function (t) {
  252. // we first initialize with empty graph:
  253. var graph = createGraph();
  254. var layout = createLayout(graph);
  255. layout.dispose();
  256. graph.addLink(1, 2);
  257. layout.step();
  258. t.ok(layout.simulator.bodies.length === 0, 'No bodies in the simulator');
  259. t.end();
  260. });
  261. test('physics simulator', function (t) {
  262. t.test('has default simulator', function (t) {
  263. var graph = createGraph();
  264. var layout = createLayout(graph);
  265. t.ok(layout.simulator, 'physics simulator is present');
  266. t.end();
  267. });
  268. t.test('can override default settings', function (t) {
  269. var graph = createGraph();
  270. var layout = createLayout(graph, {
  271. theta: 1.5
  272. });
  273. t.equal(layout.simulator.theta(), 1.5, 'Simulator settings are overridden');
  274. t.end();
  275. });
  276. t.end();
  277. });
  278. test('it removes removed nodes', function (t) {
  279. var graph = createGraph();
  280. var layout = createLayout(graph);
  281. graph.addLink(1, 2);
  282. layout.step();
  283. graph.clear();
  284. // since we removed everything from graph rect should be empty:
  285. var rect = layout.getGraphRect();
  286. t.ok(rectangleIsEmpty(rect), 'Graph rect is empty');
  287. t.end();
  288. });
  289. test('it can iterate over bodies', function(t) {
  290. var graph = createGraph();
  291. var layout = createLayout(graph);
  292. graph.addLink(1, 2);
  293. var calledCount = 0;
  294. layout.forEachBody(function(body, bodyId) {
  295. t.ok(body.pos, bodyId + ' has position');
  296. t.ok(graph.getNode(bodyId), bodyId + ' matches a graph node');
  297. calledCount += 1;
  298. });
  299. t.equal(calledCount, 2, 'Both bodies are visited');
  300. t.end();
  301. });
  302. test('it handles large graphs', function (t) {
  303. var graph = createGraph();
  304. var layout = createLayout(graph);
  305. var count = 60000;
  306. var i = count;
  307. while (i--) {
  308. graph.addNode(i);
  309. }
  310. // link each node to 2 other random nodes
  311. i = count;
  312. while (i--) {
  313. graph.addLink(i, Math.ceil(Math.random() * count));
  314. graph.addLink(i, Math.ceil(Math.random() * count));
  315. }
  316. layout.step();
  317. t.ok(layout.simulator.bodies.length !== 0, 'Bodies in the simulator');
  318. t.end();
  319. });
  320. test('it can create high dimensional layout', function(t) {
  321. var graph = createGraph();
  322. graph.addLink(1, 2);
  323. var layout = createLayout(graph, {dimensions: 6});
  324. layout.step();
  325. var pos = layout.getNodePosition(1);
  326. t.ok(pos.x !== undefined, 'Position has x');
  327. t.ok(pos.y !== undefined, 'Position has y');
  328. t.ok(pos.z !== undefined, 'Position has z');
  329. t.ok(pos.c4 !== undefined, 'Position has c4');
  330. t.ok(pos.c5 !== undefined, 'Position has c5');
  331. t.ok(pos.c6 !== undefined, 'Position has c6');
  332. t.end();
  333. });
  334. function positionChanged(pos1, pos2) {
  335. return (pos1.x !== pos2.x) || (pos1.y !== pos2.y);
  336. }
  337. function copy(obj) {
  338. return JSON.parse(JSON.stringify(obj));
  339. }
  340. function rectangleIsEmpty(rect) {
  341. return rect.min_x === 0 && rect.min_y === 0 && rect.max_x === 0 && rect.max_y === 0;
  342. }