UNPKG

ngraph.graph

Version:
581 lines (484 loc) 15.9 kB
/** * @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(); // nodeId => Node var links = new Map(); // linkId => Link // Hash of multi-edges. Used to track ids of edges between same nodes var multiEdges = {}; var suspendEvents = 0; var 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 = { /** * Sometimes duck typing could be slow. Giving clients a hint about data structure * via explicit version number here: */ version: 20.0, /** * 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, /** * Gets total number of links in the graph. */ getEdgeCount: 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 Set 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 synonym 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 synonym 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; undefined 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) { prevLinks.forEach(removeLinkInstance); node.links = null; } 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); var isUpdate = links.has(link.id); links.set(link.id, 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, isUpdate ? 'update' : 'add'); exitModification(); return link; } function createSingleLink(fromId, toId, data) { var linkId = makeLinkId(fromId, toId); var prevLink = links.get(linkId); if (prevLink) { prevLink.data = data; return prevLink; } return new Link(fromId, toId, data, linkId); } function createUniqueLink(fromId, toId, data) { // TODO: Find a better/faster way to store multigraphs 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.size; } function getLinks(nodeId) { var node = getNode(nodeId); return node ? node.links : null; } function removeLink(link, otherId) { if (otherId !== undefined) { link = getLink(link, otherId); } return removeLinkInstance(link); } function removeLinkInstance(link) { if (!link) { return false; } if (!links.get(link.id)) return false; enterModification(); links.delete(link.id); var fromNode = getNode(link.fromId); var toNode = getNode(link.toId); if (fromNode) { fromNode.links.delete(link); } if (toNode) { toNode.links.delete(link); } recordLinkChange(link, 'remove'); exitModification(); return true; } function getLink(fromNodeId, toNodeId) { if (fromNodeId === undefined || toNodeId === undefined) return undefined; return links.get(makeLinkId(fromNodeId, toNodeId)); } function clear() { enterModification(); forEachNode(function(node) { removeNode(node.id); }); exitModification(); } function forEachLink(callback) { if (typeof callback === 'function') { var valuesIterator = links.values(); var nextValue = valuesIterator.next(); while (!nextValue.done) { if (callback(nextValue.value)) { return true; // client doesn't want to proceed. Return. } nextValue = valuesIterator.next(); } } } 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); } } } // eslint-disable-next-line no-shadow function forEachNonOrientedLink(links, nodeId, callback) { var quitFast; var valuesIterator = links.values(); var nextValue = valuesIterator.next(); while (!nextValue.done) { var link = nextValue.value; 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. } nextValue = valuesIterator.next(); } } // eslint-disable-next-line no-shadow function forEachOrientedLink(links, nodeId, callback) { var quitFast; var valuesIterator = links.values(); var nextValue = valuesIterator.next(); while (!nextValue.done) { var link = nextValue.value; if (link.fromId === nodeId) { quitFast = callback(nodes.get(link.toId), link); if (quitFast) { return true; // Client does not need more iterations. Break now. } } nextValue = valuesIterator.next(); } } // 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(); } } } /** * 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.add(link); } else { node.links = new Set([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(); }