123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587 |
- /**
- * @fileOverview Contains definition of the core graph object.
- */
- // TODO: need to change storage layer:
- // 1. Be able to get all nodes O(1)
- // 2. Be able to get number of links O(1)
- /**
- * @example
- * var graph = require('ngraph.graph')();
- * graph.addNode(1); // graph has one node.
- * graph.addLink(2, 3); // now graph contains three nodes and one link.
- *
- */
- module.exports = createGraph;
- var eventify = require('ngraph.events');
- /**
- * Creates a new graph
- */
- function createGraph(options) {
- // Graph structure is maintained as dictionary of nodes
- // and array of links. Each node has 'links' property which
- // hold all links related to that node. And general links
- // array is used to speed up all links enumeration. This is inefficient
- // in terms of memory, but simplifies coding.
- options = options || {};
- if ('uniqueLinkId' in options) {
- console.warn(
- 'ngraph.graph: Starting from version 0.14 `uniqueLinkId` is deprecated.\n' +
- 'Use `multigraph` option instead\n',
- '\n',
- 'Note: there is also change in default behavior: From now on each graph\n'+
- 'is considered to be not a multigraph by default (each edge is unique).'
- );
- options.multigraph = options.uniqueLinkId;
- }
- // Dear reader, the non-multigraphs do not guarantee that there is only
- // one link for a given pair of node. When this option is set to false
- // we can save some memory and CPU (18% faster for non-multigraph);
- if (options.multigraph === undefined) options.multigraph = false;
- if (typeof Map !== 'function') {
- // TODO: Should we polyfill it ourselves? We don't use much operations there..
- throw new Error('ngraph.graph requires `Map` to be defined. Please polyfill it before using ngraph');
- }
- var nodes = new Map();
- var links = [],
- // Hash of multi-edges. Used to track ids of edges between same nodes
- multiEdges = {},
- suspendEvents = 0,
- createLink = options.multigraph ? createUniqueLink : createSingleLink,
- // Our graph API provides means to listen to graph changes. Users can subscribe
- // to be notified about changes in the graph by using `on` method. However
- // in some cases they don't use it. To avoid unnecessary memory consumption
- // we will not record graph changes until we have at least one subscriber.
- // Code below supports this optimization.
- //
- // Accumulates all changes made during graph updates.
- // Each change element contains:
- // changeType - one of the strings: 'add', 'remove' or 'update';
- // node - if change is related to node this property is set to changed graph's node;
- // link - if change is related to link this property is set to changed graph's link;
- changes = [],
- recordLinkChange = noop,
- recordNodeChange = noop,
- enterModification = noop,
- exitModification = noop;
- // this is our public API:
- var graphPart = {
- /**
- * Adds node to the graph. If node with given id already exists in the graph
- * its data is extended with whatever comes in 'data' argument.
- *
- * @param nodeId the node's identifier. A string or number is preferred.
- * @param [data] additional data for the node being added. If node already
- * exists its data object is augmented with the new one.
- *
- * @return {node} The newly added node or node with given id if it already exists.
- */
- addNode: addNode,
- /**
- * Adds a link to the graph. The function always create a new
- * link between two nodes. If one of the nodes does not exists
- * a new node is created.
- *
- * @param fromId link start node id;
- * @param toId link end node id;
- * @param [data] additional data to be set on the new link;
- *
- * @return {link} The newly created link
- */
- addLink: addLink,
- /**
- * Removes link from the graph. If link does not exist does nothing.
- *
- * @param link - object returned by addLink() or getLinks() methods.
- *
- * @returns true if link was removed; false otherwise.
- */
- removeLink: removeLink,
- /**
- * Removes node with given id from the graph. If node does not exist in the graph
- * does nothing.
- *
- * @param nodeId node's identifier passed to addNode() function.
- *
- * @returns true if node was removed; false otherwise.
- */
- removeNode: removeNode,
- /**
- * Gets node with given identifier. If node does not exist undefined value is returned.
- *
- * @param nodeId requested node identifier;
- *
- * @return {node} in with requested identifier or undefined if no such node exists.
- */
- getNode: getNode,
- /**
- * Gets number of nodes in this graph.
- *
- * @return number of nodes in the graph.
- */
- getNodeCount: getNodeCount,
- /**
- * Gets total number of links in the graph.
- */
- getLinkCount: getLinkCount,
- /**
- * Synonym for `getLinkCount()`
- */
- getLinksCount: getLinkCount,
-
- /**
- * Synonym for `getNodeCount()`
- */
- getNodesCount: getNodeCount,
- /**
- * Gets all links (inbound and outbound) from the node with given id.
- * If node with given id is not found null is returned.
- *
- * @param nodeId requested node identifier.
- *
- * @return Array of links from and to requested node if such node exists;
- * otherwise null is returned.
- */
- getLinks: getLinks,
- /**
- * Invokes callback on each node of the graph.
- *
- * @param {Function(node)} callback Function to be invoked. The function
- * is passed one argument: visited node.
- */
- forEachNode: forEachNode,
- /**
- * Invokes callback on every linked (adjacent) node to the given one.
- *
- * @param nodeId Identifier of the requested node.
- * @param {Function(node, link)} callback Function to be called on all linked nodes.
- * The function is passed two parameters: adjacent node and link object itself.
- * @param oriented if true graph treated as oriented.
- */
- forEachLinkedNode: forEachLinkedNode,
- /**
- * Enumerates all links in the graph
- *
- * @param {Function(link)} callback Function to be called on all links in the graph.
- * The function is passed one parameter: graph's link object.
- *
- * Link object contains at least the following fields:
- * fromId - node id where link starts;
- * toId - node id where link ends,
- * data - additional data passed to graph.addLink() method.
- */
- forEachLink: forEachLink,
- /**
- * Suspend all notifications about graph changes until
- * endUpdate is called.
- */
- beginUpdate: enterModification,
- /**
- * Resumes all notifications about graph changes and fires
- * graph 'changed' event in case there are any pending changes.
- */
- endUpdate: exitModification,
- /**
- * Removes all nodes and links from the graph.
- */
- clear: clear,
- /**
- * Detects whether there is a link between two nodes.
- * Operation complexity is O(n) where n - number of links of a node.
- * NOTE: this function is synonim for getLink()
- *
- * @returns link if there is one. null otherwise.
- */
- hasLink: getLink,
- /**
- * Detects whether there is a node with given id
- *
- * Operation complexity is O(1)
- * NOTE: this function is synonim for getNode()
- *
- * @returns node if there is one; Falsy value otherwise.
- */
- hasNode: getNode,
- /**
- * Gets an edge between two nodes.
- * Operation complexity is O(n) where n - number of links of a node.
- *
- * @param {string} fromId link start identifier
- * @param {string} toId link end identifier
- *
- * @returns link if there is one. null otherwise.
- */
- getLink: getLink
- };
- // this will add `on()` and `fire()` methods.
- eventify(graphPart);
- monitorSubscribers();
- return graphPart;
- function monitorSubscribers() {
- var realOn = graphPart.on;
- // replace real `on` with our temporary on, which will trigger change
- // modification monitoring:
- graphPart.on = on;
- function on() {
- // now it's time to start tracking stuff:
- graphPart.beginUpdate = enterModification = enterModificationReal;
- graphPart.endUpdate = exitModification = exitModificationReal;
- recordLinkChange = recordLinkChangeReal;
- recordNodeChange = recordNodeChangeReal;
- // this will replace current `on` method with real pub/sub from `eventify`.
- graphPart.on = realOn;
- // delegate to real `on` handler:
- return realOn.apply(graphPart, arguments);
- }
- }
- function recordLinkChangeReal(link, changeType) {
- changes.push({
- link: link,
- changeType: changeType
- });
- }
- function recordNodeChangeReal(node, changeType) {
- changes.push({
- node: node,
- changeType: changeType
- });
- }
- function addNode(nodeId, data) {
- if (nodeId === undefined) {
- throw new Error('Invalid node identifier');
- }
- enterModification();
- var node = getNode(nodeId);
- if (!node) {
- node = new Node(nodeId, data);
- recordNodeChange(node, 'add');
- } else {
- node.data = data;
- recordNodeChange(node, 'update');
- }
- nodes.set(nodeId, node);
- exitModification();
- return node;
- }
- function getNode(nodeId) {
- return nodes.get(nodeId);
- }
- function removeNode(nodeId) {
- var node = getNode(nodeId);
- if (!node) {
- return false;
- }
- enterModification();
- var prevLinks = node.links;
- if (prevLinks) {
- node.links = null;
- for(var i = 0; i < prevLinks.length; ++i) {
- removeLink(prevLinks[i]);
- }
- }
- nodes.delete(nodeId)
- recordNodeChange(node, 'remove');
- exitModification();
- return true;
- }
- function addLink(fromId, toId, data) {
- enterModification();
- var fromNode = getNode(fromId) || addNode(fromId);
- var toNode = getNode(toId) || addNode(toId);
- var link = createLink(fromId, toId, data);
- links.push(link);
- // TODO: this is not cool. On large graphs potentially would consume more memory.
- addLinkToNode(fromNode, link);
- if (fromId !== toId) {
- // make sure we are not duplicating links for self-loops
- addLinkToNode(toNode, link);
- }
- recordLinkChange(link, 'add');
- exitModification();
- return link;
- }
- function createSingleLink(fromId, toId, data) {
- var linkId = makeLinkId(fromId, toId);
- return new Link(fromId, toId, data, linkId);
- }
- function createUniqueLink(fromId, toId, data) {
- // TODO: Get rid of this method.
- var linkId = makeLinkId(fromId, toId);
- var isMultiEdge = multiEdges.hasOwnProperty(linkId);
- if (isMultiEdge || getLink(fromId, toId)) {
- if (!isMultiEdge) {
- multiEdges[linkId] = 0;
- }
- var suffix = '@' + (++multiEdges[linkId]);
- linkId = makeLinkId(fromId + suffix, toId + suffix);
- }
- return new Link(fromId, toId, data, linkId);
- }
- function getNodeCount() {
- return nodes.size;
- }
- function getLinkCount() {
- return links.length;
- }
- function getLinks(nodeId) {
- var node = getNode(nodeId);
- return node ? node.links : null;
- }
- function removeLink(link) {
- if (!link) {
- return false;
- }
- var idx = indexOfElementInArray(link, links);
- if (idx < 0) {
- return false;
- }
- enterModification();
- links.splice(idx, 1);
- var fromNode = getNode(link.fromId);
- var toNode = getNode(link.toId);
- if (fromNode) {
- idx = indexOfElementInArray(link, fromNode.links);
- if (idx >= 0) {
- fromNode.links.splice(idx, 1);
- }
- }
- if (toNode) {
- idx = indexOfElementInArray(link, toNode.links);
- if (idx >= 0) {
- toNode.links.splice(idx, 1);
- }
- }
- recordLinkChange(link, 'remove');
- exitModification();
- return true;
- }
- function getLink(fromNodeId, toNodeId) {
- // TODO: Use sorted links to speed this up
- var node = getNode(fromNodeId),
- i;
- if (!node || !node.links) {
- return null;
- }
- for (i = 0; i < node.links.length; ++i) {
- var link = node.links[i];
- if (link.fromId === fromNodeId && link.toId === toNodeId) {
- return link;
- }
- }
- return null; // no link.
- }
- function clear() {
- enterModification();
- forEachNode(function(node) {
- removeNode(node.id);
- });
- exitModification();
- }
- function forEachLink(callback) {
- var i, length;
- if (typeof callback === 'function') {
- for (i = 0, length = links.length; i < length; ++i) {
- callback(links[i]);
- }
- }
- }
- function forEachLinkedNode(nodeId, callback, oriented) {
- var node = getNode(nodeId);
- if (node && node.links && typeof callback === 'function') {
- if (oriented) {
- return forEachOrientedLink(node.links, nodeId, callback);
- } else {
- return forEachNonOrientedLink(node.links, nodeId, callback);
- }
- }
- }
- function forEachNonOrientedLink(links, nodeId, callback) {
- var quitFast;
- for (var i = 0; i < links.length; ++i) {
- var link = links[i];
- var linkedNodeId = link.fromId === nodeId ? link.toId : link.fromId;
- quitFast = callback(nodes.get(linkedNodeId), link);
- if (quitFast) {
- return true; // Client does not need more iterations. Break now.
- }
- }
- }
- function forEachOrientedLink(links, nodeId, callback) {
- var quitFast;
- for (var i = 0; i < links.length; ++i) {
- var link = links[i];
- if (link.fromId === nodeId) {
- quitFast = callback(nodes.get(link.toId), link)
- if (quitFast) {
- return true; // Client does not need more iterations. Break now.
- }
- }
- }
- }
- // we will not fire anything until users of this library explicitly call `on()`
- // method.
- function noop() {}
- // Enter, Exit modification allows bulk graph updates without firing events.
- function enterModificationReal() {
- suspendEvents += 1;
- }
- function exitModificationReal() {
- suspendEvents -= 1;
- if (suspendEvents === 0 && changes.length > 0) {
- graphPart.fire('changed', changes);
- changes.length = 0;
- }
- }
- function forEachNode(callback) {
- if (typeof callback !== 'function') {
- throw new Error('Function is expected to iterate over graph nodes. You passed ' + callback);
- }
- var valuesIterator = nodes.values();
- var nextValue = valuesIterator.next();
- while (!nextValue.done) {
- if (callback(nextValue.value)) {
- return true; // client doesn't want to proceed. Return.
- }
- nextValue = valuesIterator.next();
- }
- }
- }
- // need this for old browsers. Should this be a separate module?
- function indexOfElementInArray(element, array) {
- if (!array) return -1;
- if (array.indexOf) {
- return array.indexOf(element);
- }
- var len = array.length,
- i;
- for (i = 0; i < len; i += 1) {
- if (array[i] === element) {
- return i;
- }
- }
- return -1;
- }
- /**
- * Internal structure to represent node;
- */
- function Node(id, data) {
- this.id = id;
- this.links = null;
- this.data = data;
- }
- function addLinkToNode(node, link) {
- if (node.links) {
- node.links.push(link);
- } else {
- node.links = [link];
- }
- }
- /**
- * Internal structure to represent links;
- */
- function Link(fromId, toId, data, id) {
- this.fromId = fromId;
- this.toId = toId;
- this.data = data;
- this.id = id;
- }
- function makeLinkId(fromId, toId) {
- return fromId.toString() + '👉 ' + toId.toString();
- }
|