index.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. /**
  2. * @fileOverview Contains definition of the core graph object.
  3. */
  4. // TODO: need to change storage layer:
  5. // 1. Be able to get all nodes O(1)
  6. // 2. Be able to get number of links O(1)
  7. /**
  8. * @example
  9. * var graph = require('ngraph.graph')();
  10. * graph.addNode(1); // graph has one node.
  11. * graph.addLink(2, 3); // now graph contains three nodes and one link.
  12. *
  13. */
  14. module.exports = createGraph;
  15. var eventify = require('ngraph.events');
  16. /**
  17. * Creates a new graph
  18. */
  19. function createGraph(options) {
  20. // Graph structure is maintained as dictionary of nodes
  21. // and array of links. Each node has 'links' property which
  22. // hold all links related to that node. And general links
  23. // array is used to speed up all links enumeration. This is inefficient
  24. // in terms of memory, but simplifies coding.
  25. options = options || {};
  26. if ('uniqueLinkId' in options) {
  27. console.warn(
  28. 'ngraph.graph: Starting from version 0.14 `uniqueLinkId` is deprecated.\n' +
  29. 'Use `multigraph` option instead\n',
  30. '\n',
  31. 'Note: there is also change in default behavior: From now on each graph\n'+
  32. 'is considered to be not a multigraph by default (each edge is unique).'
  33. );
  34. options.multigraph = options.uniqueLinkId;
  35. }
  36. // Dear reader, the non-multigraphs do not guarantee that there is only
  37. // one link for a given pair of node. When this option is set to false
  38. // we can save some memory and CPU (18% faster for non-multigraph);
  39. if (options.multigraph === undefined) options.multigraph = false;
  40. if (typeof Map !== 'function') {
  41. // TODO: Should we polyfill it ourselves? We don't use much operations there..
  42. throw new Error('ngraph.graph requires `Map` to be defined. Please polyfill it before using ngraph');
  43. }
  44. var nodes = new Map();
  45. var links = [],
  46. // Hash of multi-edges. Used to track ids of edges between same nodes
  47. multiEdges = {},
  48. suspendEvents = 0,
  49. createLink = options.multigraph ? createUniqueLink : createSingleLink,
  50. // Our graph API provides means to listen to graph changes. Users can subscribe
  51. // to be notified about changes in the graph by using `on` method. However
  52. // in some cases they don't use it. To avoid unnecessary memory consumption
  53. // we will not record graph changes until we have at least one subscriber.
  54. // Code below supports this optimization.
  55. //
  56. // Accumulates all changes made during graph updates.
  57. // Each change element contains:
  58. // changeType - one of the strings: 'add', 'remove' or 'update';
  59. // node - if change is related to node this property is set to changed graph's node;
  60. // link - if change is related to link this property is set to changed graph's link;
  61. changes = [],
  62. recordLinkChange = noop,
  63. recordNodeChange = noop,
  64. enterModification = noop,
  65. exitModification = noop;
  66. // this is our public API:
  67. var graphPart = {
  68. /**
  69. * Adds node to the graph. If node with given id already exists in the graph
  70. * its data is extended with whatever comes in 'data' argument.
  71. *
  72. * @param nodeId the node's identifier. A string or number is preferred.
  73. * @param [data] additional data for the node being added. If node already
  74. * exists its data object is augmented with the new one.
  75. *
  76. * @return {node} The newly added node or node with given id if it already exists.
  77. */
  78. addNode: addNode,
  79. /**
  80. * Adds a link to the graph. The function always create a new
  81. * link between two nodes. If one of the nodes does not exists
  82. * a new node is created.
  83. *
  84. * @param fromId link start node id;
  85. * @param toId link end node id;
  86. * @param [data] additional data to be set on the new link;
  87. *
  88. * @return {link} The newly created link
  89. */
  90. addLink: addLink,
  91. /**
  92. * Removes link from the graph. If link does not exist does nothing.
  93. *
  94. * @param link - object returned by addLink() or getLinks() methods.
  95. *
  96. * @returns true if link was removed; false otherwise.
  97. */
  98. removeLink: removeLink,
  99. /**
  100. * Removes node with given id from the graph. If node does not exist in the graph
  101. * does nothing.
  102. *
  103. * @param nodeId node's identifier passed to addNode() function.
  104. *
  105. * @returns true if node was removed; false otherwise.
  106. */
  107. removeNode: removeNode,
  108. /**
  109. * Gets node with given identifier. If node does not exist undefined value is returned.
  110. *
  111. * @param nodeId requested node identifier;
  112. *
  113. * @return {node} in with requested identifier or undefined if no such node exists.
  114. */
  115. getNode: getNode,
  116. /**
  117. * Gets number of nodes in this graph.
  118. *
  119. * @return number of nodes in the graph.
  120. */
  121. getNodeCount: getNodeCount,
  122. /**
  123. * Gets total number of links in the graph.
  124. */
  125. getLinkCount: getLinkCount,
  126. /**
  127. * Synonym for `getLinkCount()`
  128. */
  129. getLinksCount: getLinkCount,
  130. /**
  131. * Synonym for `getNodeCount()`
  132. */
  133. getNodesCount: getNodeCount,
  134. /**
  135. * Gets all links (inbound and outbound) from the node with given id.
  136. * If node with given id is not found null is returned.
  137. *
  138. * @param nodeId requested node identifier.
  139. *
  140. * @return Array of links from and to requested node if such node exists;
  141. * otherwise null is returned.
  142. */
  143. getLinks: getLinks,
  144. /**
  145. * Invokes callback on each node of the graph.
  146. *
  147. * @param {Function(node)} callback Function to be invoked. The function
  148. * is passed one argument: visited node.
  149. */
  150. forEachNode: forEachNode,
  151. /**
  152. * Invokes callback on every linked (adjacent) node to the given one.
  153. *
  154. * @param nodeId Identifier of the requested node.
  155. * @param {Function(node, link)} callback Function to be called on all linked nodes.
  156. * The function is passed two parameters: adjacent node and link object itself.
  157. * @param oriented if true graph treated as oriented.
  158. */
  159. forEachLinkedNode: forEachLinkedNode,
  160. /**
  161. * Enumerates all links in the graph
  162. *
  163. * @param {Function(link)} callback Function to be called on all links in the graph.
  164. * The function is passed one parameter: graph's link object.
  165. *
  166. * Link object contains at least the following fields:
  167. * fromId - node id where link starts;
  168. * toId - node id where link ends,
  169. * data - additional data passed to graph.addLink() method.
  170. */
  171. forEachLink: forEachLink,
  172. /**
  173. * Suspend all notifications about graph changes until
  174. * endUpdate is called.
  175. */
  176. beginUpdate: enterModification,
  177. /**
  178. * Resumes all notifications about graph changes and fires
  179. * graph 'changed' event in case there are any pending changes.
  180. */
  181. endUpdate: exitModification,
  182. /**
  183. * Removes all nodes and links from the graph.
  184. */
  185. clear: clear,
  186. /**
  187. * Detects whether there is a link between two nodes.
  188. * Operation complexity is O(n) where n - number of links of a node.
  189. * NOTE: this function is synonim for getLink()
  190. *
  191. * @returns link if there is one. null otherwise.
  192. */
  193. hasLink: getLink,
  194. /**
  195. * Detects whether there is a node with given id
  196. *
  197. * Operation complexity is O(1)
  198. * NOTE: this function is synonim for getNode()
  199. *
  200. * @returns node if there is one; Falsy value otherwise.
  201. */
  202. hasNode: getNode,
  203. /**
  204. * Gets an edge between two nodes.
  205. * Operation complexity is O(n) where n - number of links of a node.
  206. *
  207. * @param {string} fromId link start identifier
  208. * @param {string} toId link end identifier
  209. *
  210. * @returns link if there is one. null otherwise.
  211. */
  212. getLink: getLink
  213. };
  214. // this will add `on()` and `fire()` methods.
  215. eventify(graphPart);
  216. monitorSubscribers();
  217. return graphPart;
  218. function monitorSubscribers() {
  219. var realOn = graphPart.on;
  220. // replace real `on` with our temporary on, which will trigger change
  221. // modification monitoring:
  222. graphPart.on = on;
  223. function on() {
  224. // now it's time to start tracking stuff:
  225. graphPart.beginUpdate = enterModification = enterModificationReal;
  226. graphPart.endUpdate = exitModification = exitModificationReal;
  227. recordLinkChange = recordLinkChangeReal;
  228. recordNodeChange = recordNodeChangeReal;
  229. // this will replace current `on` method with real pub/sub from `eventify`.
  230. graphPart.on = realOn;
  231. // delegate to real `on` handler:
  232. return realOn.apply(graphPart, arguments);
  233. }
  234. }
  235. function recordLinkChangeReal(link, changeType) {
  236. changes.push({
  237. link: link,
  238. changeType: changeType
  239. });
  240. }
  241. function recordNodeChangeReal(node, changeType) {
  242. changes.push({
  243. node: node,
  244. changeType: changeType
  245. });
  246. }
  247. function addNode(nodeId, data) {
  248. if (nodeId === undefined) {
  249. throw new Error('Invalid node identifier');
  250. }
  251. enterModification();
  252. var node = getNode(nodeId);
  253. if (!node) {
  254. node = new Node(nodeId, data);
  255. recordNodeChange(node, 'add');
  256. } else {
  257. node.data = data;
  258. recordNodeChange(node, 'update');
  259. }
  260. nodes.set(nodeId, node);
  261. exitModification();
  262. return node;
  263. }
  264. function getNode(nodeId) {
  265. return nodes.get(nodeId);
  266. }
  267. function removeNode(nodeId) {
  268. var node = getNode(nodeId);
  269. if (!node) {
  270. return false;
  271. }
  272. enterModification();
  273. var prevLinks = node.links;
  274. if (prevLinks) {
  275. node.links = null;
  276. for(var i = 0; i < prevLinks.length; ++i) {
  277. removeLink(prevLinks[i]);
  278. }
  279. }
  280. nodes.delete(nodeId)
  281. recordNodeChange(node, 'remove');
  282. exitModification();
  283. return true;
  284. }
  285. function addLink(fromId, toId, data) {
  286. enterModification();
  287. var fromNode = getNode(fromId) || addNode(fromId);
  288. var toNode = getNode(toId) || addNode(toId);
  289. var link = createLink(fromId, toId, data);
  290. links.push(link);
  291. // TODO: this is not cool. On large graphs potentially would consume more memory.
  292. addLinkToNode(fromNode, link);
  293. if (fromId !== toId) {
  294. // make sure we are not duplicating links for self-loops
  295. addLinkToNode(toNode, link);
  296. }
  297. recordLinkChange(link, 'add');
  298. exitModification();
  299. return link;
  300. }
  301. function createSingleLink(fromId, toId, data) {
  302. var linkId = makeLinkId(fromId, toId);
  303. return new Link(fromId, toId, data, linkId);
  304. }
  305. function createUniqueLink(fromId, toId, data) {
  306. // TODO: Get rid of this method.
  307. var linkId = makeLinkId(fromId, toId);
  308. var isMultiEdge = multiEdges.hasOwnProperty(linkId);
  309. if (isMultiEdge || getLink(fromId, toId)) {
  310. if (!isMultiEdge) {
  311. multiEdges[linkId] = 0;
  312. }
  313. var suffix = '@' + (++multiEdges[linkId]);
  314. linkId = makeLinkId(fromId + suffix, toId + suffix);
  315. }
  316. return new Link(fromId, toId, data, linkId);
  317. }
  318. function getNodeCount() {
  319. return nodes.size;
  320. }
  321. function getLinkCount() {
  322. return links.length;
  323. }
  324. function getLinks(nodeId) {
  325. var node = getNode(nodeId);
  326. return node ? node.links : null;
  327. }
  328. function removeLink(link) {
  329. if (!link) {
  330. return false;
  331. }
  332. var idx = indexOfElementInArray(link, links);
  333. if (idx < 0) {
  334. return false;
  335. }
  336. enterModification();
  337. links.splice(idx, 1);
  338. var fromNode = getNode(link.fromId);
  339. var toNode = getNode(link.toId);
  340. if (fromNode) {
  341. idx = indexOfElementInArray(link, fromNode.links);
  342. if (idx >= 0) {
  343. fromNode.links.splice(idx, 1);
  344. }
  345. }
  346. if (toNode) {
  347. idx = indexOfElementInArray(link, toNode.links);
  348. if (idx >= 0) {
  349. toNode.links.splice(idx, 1);
  350. }
  351. }
  352. recordLinkChange(link, 'remove');
  353. exitModification();
  354. return true;
  355. }
  356. function getLink(fromNodeId, toNodeId) {
  357. // TODO: Use sorted links to speed this up
  358. var node = getNode(fromNodeId),
  359. i;
  360. if (!node || !node.links) {
  361. return null;
  362. }
  363. for (i = 0; i < node.links.length; ++i) {
  364. var link = node.links[i];
  365. if (link.fromId === fromNodeId && link.toId === toNodeId) {
  366. return link;
  367. }
  368. }
  369. return null; // no link.
  370. }
  371. function clear() {
  372. enterModification();
  373. forEachNode(function(node) {
  374. removeNode(node.id);
  375. });
  376. exitModification();
  377. }
  378. function forEachLink(callback) {
  379. var i, length;
  380. if (typeof callback === 'function') {
  381. for (i = 0, length = links.length; i < length; ++i) {
  382. callback(links[i]);
  383. }
  384. }
  385. }
  386. function forEachLinkedNode(nodeId, callback, oriented) {
  387. var node = getNode(nodeId);
  388. if (node && node.links && typeof callback === 'function') {
  389. if (oriented) {
  390. return forEachOrientedLink(node.links, nodeId, callback);
  391. } else {
  392. return forEachNonOrientedLink(node.links, nodeId, callback);
  393. }
  394. }
  395. }
  396. function forEachNonOrientedLink(links, nodeId, callback) {
  397. var quitFast;
  398. for (var i = 0; i < links.length; ++i) {
  399. var link = links[i];
  400. var linkedNodeId = link.fromId === nodeId ? link.toId : link.fromId;
  401. quitFast = callback(nodes.get(linkedNodeId), link);
  402. if (quitFast) {
  403. return true; // Client does not need more iterations. Break now.
  404. }
  405. }
  406. }
  407. function forEachOrientedLink(links, nodeId, callback) {
  408. var quitFast;
  409. for (var i = 0; i < links.length; ++i) {
  410. var link = links[i];
  411. if (link.fromId === nodeId) {
  412. quitFast = callback(nodes.get(link.toId), link)
  413. if (quitFast) {
  414. return true; // Client does not need more iterations. Break now.
  415. }
  416. }
  417. }
  418. }
  419. // we will not fire anything until users of this library explicitly call `on()`
  420. // method.
  421. function noop() {}
  422. // Enter, Exit modification allows bulk graph updates without firing events.
  423. function enterModificationReal() {
  424. suspendEvents += 1;
  425. }
  426. function exitModificationReal() {
  427. suspendEvents -= 1;
  428. if (suspendEvents === 0 && changes.length > 0) {
  429. graphPart.fire('changed', changes);
  430. changes.length = 0;
  431. }
  432. }
  433. function forEachNode(callback) {
  434. if (typeof callback !== 'function') {
  435. throw new Error('Function is expected to iterate over graph nodes. You passed ' + callback);
  436. }
  437. var valuesIterator = nodes.values();
  438. var nextValue = valuesIterator.next();
  439. while (!nextValue.done) {
  440. if (callback(nextValue.value)) {
  441. return true; // client doesn't want to proceed. Return.
  442. }
  443. nextValue = valuesIterator.next();
  444. }
  445. }
  446. }
  447. // need this for old browsers. Should this be a separate module?
  448. function indexOfElementInArray(element, array) {
  449. if (!array) return -1;
  450. if (array.indexOf) {
  451. return array.indexOf(element);
  452. }
  453. var len = array.length,
  454. i;
  455. for (i = 0; i < len; i += 1) {
  456. if (array[i] === element) {
  457. return i;
  458. }
  459. }
  460. return -1;
  461. }
  462. /**
  463. * Internal structure to represent node;
  464. */
  465. function Node(id, data) {
  466. this.id = id;
  467. this.links = null;
  468. this.data = data;
  469. }
  470. function addLinkToNode(node, link) {
  471. if (node.links) {
  472. node.links.push(link);
  473. } else {
  474. node.links = [link];
  475. }
  476. }
  477. /**
  478. * Internal structure to represent links;
  479. */
  480. function Link(fromId, toId, data, id) {
  481. this.fromId = fromId;
  482. this.toId = toId;
  483. this.data = data;
  484. this.id = id;
  485. }
  486. function makeLinkId(fromId, toId) {
  487. return fromId.toString() + '👉 ' + toId.toString();
  488. }