ngraph.graph
Version:
graph data structure
581 lines (484 loc) • 15.9 kB
JavaScript
/**
* @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();
}