index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. module.exports = createLayout;
  2. module.exports.simulator = require('./lib/createPhysicsSimulator');
  3. var eventify = require('ngraph.events');
  4. /**
  5. * Creates force based layout for a given graph.
  6. *
  7. * @param {ngraph.graph} graph which needs to be laid out
  8. * @param {object} physicsSettings if you need custom settings
  9. * for physics simulator you can pass your own settings here. If it's not passed
  10. * a default one will be created.
  11. */
  12. function createLayout(graph, physicsSettings) {
  13. if (!graph) {
  14. throw new Error('Graph structure cannot be undefined');
  15. }
  16. var createSimulator = (physicsSettings && physicsSettings.createSimulator) || require('./lib/createPhysicsSimulator');
  17. var physicsSimulator = createSimulator(physicsSettings);
  18. if (Array.isArray(physicsSettings)) throw new Error('Physics settings is expected to be an object');
  19. var nodeMass = graph.version > 19 ? defaultSetNodeMass : defaultArrayNodeMass;
  20. if (physicsSettings && typeof physicsSettings.nodeMass === 'function') {
  21. nodeMass = physicsSettings.nodeMass;
  22. }
  23. var nodeBodies = new Map();
  24. var springs = {};
  25. var bodiesCount = 0;
  26. var springTransform = physicsSimulator.settings.springTransform || noop;
  27. // Initialize physics with what we have in the graph:
  28. initPhysics();
  29. listenToEvents();
  30. var wasStable = false;
  31. var api = {
  32. /**
  33. * Performs one step of iterative layout algorithm
  34. *
  35. * @returns {boolean} true if the system should be considered stable; False otherwise.
  36. * The system is stable if no further call to `step()` can improve the layout.
  37. */
  38. step: function() {
  39. if (bodiesCount === 0) {
  40. updateStableStatus(true);
  41. return true;
  42. }
  43. var lastMove = physicsSimulator.step();
  44. // Save the movement in case if someone wants to query it in the step
  45. // callback.
  46. api.lastMove = lastMove;
  47. // Allow listeners to perform low-level actions after nodes are updated.
  48. api.fire('step');
  49. var ratio = lastMove/bodiesCount;
  50. var isStableNow = ratio <= 0.01; // TODO: The number is somewhat arbitrary...
  51. updateStableStatus(isStableNow);
  52. return isStableNow;
  53. },
  54. /**
  55. * For a given `nodeId` returns position
  56. */
  57. getNodePosition: function (nodeId) {
  58. return getInitializedBody(nodeId).pos;
  59. },
  60. /**
  61. * Sets position of a node to a given coordinates
  62. * @param {string} nodeId node identifier
  63. * @param {number} x position of a node
  64. * @param {number} y position of a node
  65. * @param {number=} z position of node (only if applicable to body)
  66. */
  67. setNodePosition: function (nodeId) {
  68. var body = getInitializedBody(nodeId);
  69. body.setPosition.apply(body, Array.prototype.slice.call(arguments, 1));
  70. },
  71. /**
  72. * @returns {Object} Link position by link id
  73. * @returns {Object.from} {x, y} coordinates of link start
  74. * @returns {Object.to} {x, y} coordinates of link end
  75. */
  76. getLinkPosition: function (linkId) {
  77. var spring = springs[linkId];
  78. if (spring) {
  79. return {
  80. from: spring.from.pos,
  81. to: spring.to.pos
  82. };
  83. }
  84. },
  85. /**
  86. * @returns {Object} area required to fit in the graph. Object contains
  87. * `x1`, `y1` - top left coordinates
  88. * `x2`, `y2` - bottom right coordinates
  89. */
  90. getGraphRect: function () {
  91. return physicsSimulator.getBBox();
  92. },
  93. /**
  94. * Iterates over each body in the layout simulator and performs a callback(body, nodeId)
  95. */
  96. forEachBody: forEachBody,
  97. /*
  98. * Requests layout algorithm to pin/unpin node to its current position
  99. * Pinned nodes should not be affected by layout algorithm and always
  100. * remain at their position
  101. */
  102. pinNode: function (node, isPinned) {
  103. var body = getInitializedBody(node.id);
  104. body.isPinned = !!isPinned;
  105. },
  106. /**
  107. * Checks whether given graph's node is currently pinned
  108. */
  109. isNodePinned: function (node) {
  110. return getInitializedBody(node.id).isPinned;
  111. },
  112. /**
  113. * Request to release all resources
  114. */
  115. dispose: function() {
  116. graph.off('changed', onGraphChanged);
  117. api.fire('disposed');
  118. },
  119. /**
  120. * Gets physical body for a given node id. If node is not found undefined
  121. * value is returned.
  122. */
  123. getBody: getBody,
  124. /**
  125. * Gets spring for a given edge.
  126. *
  127. * @param {string} linkId link identifer. If two arguments are passed then
  128. * this argument is treated as formNodeId
  129. * @param {string=} toId when defined this parameter denotes head of the link
  130. * and first argument is treated as tail of the link (fromId)
  131. */
  132. getSpring: getSpring,
  133. /**
  134. * Returns length of cumulative force vector. The closer this to zero - the more stable the system is
  135. */
  136. getForceVectorLength: getForceVectorLength,
  137. /**
  138. * [Read only] Gets current physics simulator
  139. */
  140. simulator: physicsSimulator,
  141. /**
  142. * Gets the graph that was used for layout
  143. */
  144. graph: graph,
  145. /**
  146. * Gets amount of movement performed during last step operation
  147. */
  148. lastMove: 0
  149. };
  150. eventify(api);
  151. return api;
  152. function updateStableStatus(isStableNow) {
  153. if (wasStable !== isStableNow) {
  154. wasStable = isStableNow;
  155. onStableChanged(isStableNow);
  156. }
  157. }
  158. function forEachBody(cb) {
  159. nodeBodies.forEach(cb);
  160. }
  161. function getForceVectorLength() {
  162. var fx = 0, fy = 0;
  163. forEachBody(function(body) {
  164. fx += Math.abs(body.force.x);
  165. fy += Math.abs(body.force.y);
  166. });
  167. return Math.sqrt(fx * fx + fy * fy);
  168. }
  169. function getSpring(fromId, toId) {
  170. var linkId;
  171. if (toId === undefined) {
  172. if (typeof fromId !== 'object') {
  173. // assume fromId as a linkId:
  174. linkId = fromId;
  175. } else {
  176. // assume fromId to be a link object:
  177. linkId = fromId.id;
  178. }
  179. } else {
  180. // toId is defined, should grab link:
  181. var link = graph.hasLink(fromId, toId);
  182. if (!link) return;
  183. linkId = link.id;
  184. }
  185. return springs[linkId];
  186. }
  187. function getBody(nodeId) {
  188. return nodeBodies.get(nodeId);
  189. }
  190. function listenToEvents() {
  191. graph.on('changed', onGraphChanged);
  192. }
  193. function onStableChanged(isStable) {
  194. api.fire('stable', isStable);
  195. }
  196. function onGraphChanged(changes) {
  197. for (var i = 0; i < changes.length; ++i) {
  198. var change = changes[i];
  199. if (change.changeType === 'add') {
  200. if (change.node) {
  201. initBody(change.node.id);
  202. }
  203. if (change.link) {
  204. initLink(change.link);
  205. }
  206. } else if (change.changeType === 'remove') {
  207. if (change.node) {
  208. releaseNode(change.node);
  209. }
  210. if (change.link) {
  211. releaseLink(change.link);
  212. }
  213. }
  214. }
  215. bodiesCount = graph.getNodesCount();
  216. }
  217. function initPhysics() {
  218. bodiesCount = 0;
  219. graph.forEachNode(function (node) {
  220. initBody(node.id);
  221. bodiesCount += 1;
  222. });
  223. graph.forEachLink(initLink);
  224. }
  225. function initBody(nodeId) {
  226. var body = nodeBodies.get(nodeId);
  227. if (!body) {
  228. var node = graph.getNode(nodeId);
  229. if (!node) {
  230. throw new Error('initBody() was called with unknown node id');
  231. }
  232. var pos = node.position;
  233. if (!pos) {
  234. var neighbors = getNeighborBodies(node);
  235. pos = physicsSimulator.getBestNewBodyPosition(neighbors);
  236. }
  237. body = physicsSimulator.addBodyAt(pos);
  238. body.id = nodeId;
  239. nodeBodies.set(nodeId, body);
  240. updateBodyMass(nodeId);
  241. if (isNodeOriginallyPinned(node)) {
  242. body.isPinned = true;
  243. }
  244. }
  245. }
  246. function releaseNode(node) {
  247. var nodeId = node.id;
  248. var body = nodeBodies.get(nodeId);
  249. if (body) {
  250. nodeBodies.delete(nodeId);
  251. physicsSimulator.removeBody(body);
  252. }
  253. }
  254. function initLink(link) {
  255. updateBodyMass(link.fromId);
  256. updateBodyMass(link.toId);
  257. var fromBody = nodeBodies.get(link.fromId),
  258. toBody = nodeBodies.get(link.toId),
  259. spring = physicsSimulator.addSpring(fromBody, toBody, link.length);
  260. springTransform(link, spring);
  261. springs[link.id] = spring;
  262. }
  263. function releaseLink(link) {
  264. var spring = springs[link.id];
  265. if (spring) {
  266. var from = graph.getNode(link.fromId),
  267. to = graph.getNode(link.toId);
  268. if (from) updateBodyMass(from.id);
  269. if (to) updateBodyMass(to.id);
  270. delete springs[link.id];
  271. physicsSimulator.removeSpring(spring);
  272. }
  273. }
  274. function getNeighborBodies(node) {
  275. // TODO: Could probably be done better on memory
  276. var neighbors = [];
  277. if (!node.links) {
  278. return neighbors;
  279. }
  280. var maxNeighbors = Math.min(node.links.length, 2);
  281. for (var i = 0; i < maxNeighbors; ++i) {
  282. var link = node.links[i];
  283. var otherBody = link.fromId !== node.id ? nodeBodies.get(link.fromId) : nodeBodies.get(link.toId);
  284. if (otherBody && otherBody.pos) {
  285. neighbors.push(otherBody);
  286. }
  287. }
  288. return neighbors;
  289. }
  290. function updateBodyMass(nodeId) {
  291. var body = nodeBodies.get(nodeId);
  292. body.mass = nodeMass(nodeId);
  293. if (Number.isNaN(body.mass)) {
  294. throw new Error('Node mass should be a number');
  295. }
  296. }
  297. /**
  298. * Checks whether graph node has in its settings pinned attribute,
  299. * which means layout algorithm cannot move it. Node can be marked
  300. * as pinned, if it has "isPinned" attribute, or when node.data has it.
  301. *
  302. * @param {Object} node a graph node to check
  303. * @return {Boolean} true if node should be treated as pinned; false otherwise.
  304. */
  305. function isNodeOriginallyPinned(node) {
  306. return (node && (node.isPinned || (node.data && node.data.isPinned)));
  307. }
  308. function getInitializedBody(nodeId) {
  309. var body = nodeBodies.get(nodeId);
  310. if (!body) {
  311. initBody(nodeId);
  312. body = nodeBodies.get(nodeId);
  313. }
  314. return body;
  315. }
  316. /**
  317. * Calculates mass of a body, which corresponds to node with given id.
  318. *
  319. * @param {String|Number} nodeId identifier of a node, for which body mass needs to be calculated
  320. * @returns {Number} recommended mass of the body;
  321. */
  322. function defaultArrayNodeMass(nodeId) {
  323. // This function is for older versions of ngraph.graph.
  324. var links = graph.getLinks(nodeId);
  325. if (!links) return 1;
  326. return 1 + links.length / 3.0;
  327. }
  328. function defaultSetNodeMass(nodeId) {
  329. var links = graph.getLinks(nodeId);
  330. if (!links) return 1;
  331. return 1 + links.size / 3.0;
  332. }
  333. }
  334. function noop() { }