UNPKG

graphology

Version:

A robust and multipurpose Graph object for JavaScript.

1,937 lines (1,595 loc) 162 kB
import { EventEmitter } from 'events'; import Iterator from 'obliterator/iterator'; import take from 'obliterator/take'; import chain from 'obliterator/chain'; /** * Graphology Utilities * ===================== * * Collection of helpful functions used by the implementation. */ /** * Object.assign-like polyfill. * * @param {object} target - First object. * @param {object} [...objects] - Objects to merge. * @return {object} */ function assignPolyfill() { const target = arguments[0]; for (let i = 1, l = arguments.length; i < l; i++) { if (!arguments[i]) continue; for (const k in arguments[i]) target[k] = arguments[i][k]; } return target; } let assign = assignPolyfill; if (typeof Object.assign === 'function') assign = Object.assign; /** * Function returning the first matching edge for given path. * Note: this function does not check the existence of source & target. This * must be performed by the caller. * * @param {Graph} graph - Target graph. * @param {any} source - Source node. * @param {any} target - Target node. * @param {string} type - Type of the edge (mixed, directed or undirected). * @return {string|null} */ function getMatchingEdge(graph, source, target, type) { const sourceData = graph._nodes.get(source); let edge = null; if (!sourceData) return edge; if (type === 'mixed') { edge = ( (sourceData.out && sourceData.out[target]) || (sourceData.undirected && sourceData.undirected[target]) ); } else if (type === 'directed') { edge = sourceData.out && sourceData.out[target]; } else { edge = sourceData.undirected && sourceData.undirected[target]; } return edge; } /** * Checks whether the given value is a Graph implementation instance. * * @param {mixed} value - Target value. * @return {boolean} */ function isGraph(value) { return ( value !== null && typeof value === 'object' && typeof value.addUndirectedEdgeWithKey === 'function' && typeof value.dropNode === 'function' ); } /** * Checks whether the given value is a plain object. * * @param {mixed} value - Target value. * @return {boolean} */ function isPlainObject(value) { return ( typeof value === 'object' && value !== null && value.constructor === Object ); } /** * Checks whether the given object is empty. * * @param {object} o - Target Object. * @return {boolean} */ function isEmpty(o) { let k; for (k in o) return false; return true; } /** * Creates a "private" property for the given member name by concealing it * using the `enumerable` option. * * @param {object} target - Target object. * @param {string} name - Member name. */ function privateProperty(target, name, value) { Object.defineProperty(target, name, { enumerable: false, configurable: false, writable: true, value }); } /** * Creates a read-only property for the given member name & the given getter. * * @param {object} target - Target object. * @param {string} name - Member name. * @param {mixed} value - The attached getter or fixed value. */ function readOnlyProperty(target, name, value) { const descriptor = { enumerable: true, configurable: true }; if (typeof value === 'function') { descriptor.get = value; } else { descriptor.value = value; descriptor.writable = false; } Object.defineProperty(target, name, descriptor); } /** * Returns whether the given object constitute valid hints. * * @param {object} hints - Target object. */ function validateHints(hints) { if (!isPlainObject(hints)) return false; if (hints.attributes && !Array.isArray(hints.attributes)) return false; return true; } /** * Creates a function generating incremental ids for edges. * * @return {function} */ function incrementalId() { let i = 0; return () => { return i++; }; } /** * Graphology Custom Errors * ========================= * * Defining custom errors for ease of use & easy unit tests across * implementations (normalized typology rather than relying on error * messages to check whether the correct error was found). */ class GraphError extends Error { constructor(message, data) { super(); this.name = 'GraphError'; this.message = message || ''; this.data = data || {}; } } class InvalidArgumentsGraphError extends GraphError { constructor(message, data) { super(message, data); this.name = 'InvalidArgumentsGraphError'; // This is V8 specific to enhance stack readability if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(this, InvalidArgumentsGraphError.prototype.constructor); } } class NotFoundGraphError extends GraphError { constructor(message, data) { super(message, data); this.name = 'NotFoundGraphError'; // This is V8 specific to enhance stack readability if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(this, NotFoundGraphError.prototype.constructor); } } class UsageGraphError extends GraphError { constructor(message, data) { super(message, data); this.name = 'UsageGraphError'; // This is V8 specific to enhance stack readability if (typeof Error.captureStackTrace === 'function') Error.captureStackTrace(this, UsageGraphError.prototype.constructor); } } /** * Graphology Internal Data Classes * ================================= * * Internal classes hopefully reduced to structs by engines & storing * necessary information for nodes & edges. * * Note that those classes don't rely on the `class` keyword to avoid some * cruft introduced by most of ES2015 transpilers. */ /** * MixedNodeData class. * * @constructor * @param {string} string - The node's key. * @param {object} attributes - Node's attributes. */ function MixedNodeData(key, attributes) { // Attributes this.key = key; this.attributes = attributes; // Degrees this.inDegree = 0; this.outDegree = 0; this.undirectedDegree = 0; this.directedSelfLoops = 0; this.undirectedSelfLoops = 0; // Indices this.in = {}; this.out = {}; this.undirected = {}; } /** * DirectedNodeData class. * * @constructor * @param {string} string - The node's key. * @param {object} attributes - Node's attributes. */ function DirectedNodeData(key, attributes) { // Attributes this.key = key; this.attributes = attributes; // Degrees this.inDegree = 0; this.outDegree = 0; this.directedSelfLoops = 0; // Indices this.in = {}; this.out = {}; } DirectedNodeData.prototype.upgradeToMixed = function() { // Degrees this.undirectedDegree = 0; this.undirectedSelfLoops = 0; // Indices this.undirected = {}; }; /** * UndirectedNodeData class. * * @constructor * @param {string} string - The node's key. * @param {object} attributes - Node's attributes. */ function UndirectedNodeData(key, attributes) { // Attributes this.key = key; this.attributes = attributes; // Degrees this.undirectedDegree = 0; this.undirectedSelfLoops = 0; // Indices this.undirected = {}; } UndirectedNodeData.prototype.upgradeToMixed = function() { // Degrees this.inDegree = 0; this.outDegree = 0; this.directedSelfLoops = 0; // Indices this.in = {}; this.out = {}; }; /** * EdgeData class. * * @constructor * @param {boolean} undirected - Whether the edge is undirected. * @param {string} string - The edge's key. * @param {boolean} generatedKey - Was its key generated? * @param {string} source - Source of the edge. * @param {string} target - Target of the edge. * @param {object} attributes - Edge's attributes. */ function EdgeData(undirected, key, generatedKey, source, target, attributes) { // Attributes this.key = key; this.attributes = attributes; this.undirected = undirected; // Extremities this.source = source; this.target = target; // Was its key generated? this.generatedKey = generatedKey; } /** * Graphology Indexes Functions * ============================= * * Bunch of functions used to compute or clear indexes. */ /** * Function updating the 'structure' index with the given edge's data. * Note that in the case of the multi graph, related edges are stored in a * set that is the same for A -> B & B <- A. * * @param {Graph} graph - Target Graph instance. * @param {EdgeData} edgeData - Added edge's data. * @param {NodeData} sourceData - Source node's data. * @param {NodeData} targetData - Target node's data. */ function updateStructureIndex( graph, undirected, edgeData, source, target, sourceData, targetData ) { const multi = graph.multi; let outKey = 'out'; let inKey = 'in'; if (undirected) outKey = inKey = 'undirected'; let adj, container; if (multi) { // Handling source adj = sourceData[outKey]; container = adj[target]; if (typeof container === 'undefined') { container = new Set(); adj[target] = container; } container.add(edgeData); // If selfLoop, we break here if (source === target && undirected) return; // Handling target (we won't add the edge because it was already taken // care of with source above) adj = targetData[inKey]; if (typeof adj[source] === 'undefined') adj[source] = container; } else { // Handling source sourceData[outKey][target] = edgeData; // If selfLoop, we break here if (source === target && undirected) return; // Handling target targetData[inKey][source] = edgeData; } } /** * Function clearing the 'structure' index data related to the given edge. * * @param {Graph} graph - Target Graph instance. * @param {EdgeData} edgeData - Dropped edge's data. */ function clearEdgeFromStructureIndex(graph, undirected, edgeData) { const multi = graph.multi; const {source: sourceData, target: targetData} = edgeData; const source = sourceData.key, target = targetData.key; // NOTE: since the edge set is the same for source & target, we can only // affect source const outKey = undirected ? 'undirected' : 'out', sourceIndex = sourceData[outKey]; const inKey = undirected ? 'undirected' : 'in'; if (target in sourceIndex) { if (multi) { const set = sourceIndex[target]; if (set.size === 1) { delete sourceIndex[target]; delete targetData[inKey][source]; } else { set.delete(edgeData); } } else delete sourceIndex[target]; } if (multi) return; const targetIndex = targetData[inKey]; delete targetIndex[source]; } /** * Function clearing the whole 'structure' index. * * @param {Graph} graph - Target Graph instance. */ function clearStructureIndex(graph) { graph._nodes.forEach(data => { // Clearing now useless properties if (typeof data.in !== 'undefined') { data.in = {}; data.out = {}; } if (typeof data.undirected !== 'undefined') { data.undirected = {}; } }); } /** * Function used to upgrade a simple `structure` index to a multi on. * * @param {Graph} graph - Target Graph instance. */ function upgradeStructureIndexToMulti(graph) { graph._nodes.forEach((data, node) => { // Directed if (data.out) { for (const neighbor in data.out) { const edges = new Set(); edges.add(data.out[neighbor]); data.out[neighbor] = edges; graph._nodes.get(neighbor).in[node] = edges; } } // Undirected if (data.undirected) { for (const neighbor in data.undirected) { if (neighbor > node) continue; const edges = new Set(); edges.add(data.undirected[neighbor]); data.undirected[neighbor] = edges; graph._nodes.get(neighbor).undirected[node] = edges; } } }); } /** * Graphology Attributes methods * ============================== * * Attributes-related methods being exactly the same for nodes & edges, * we abstract them here for factorization reasons. */ /** * Attach an attribute getter method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachAttributeGetter(Class, method, type) { /** * Get the desired attribute for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {string} name - Attribute's name. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {string} name - Attribute's name. * * @return {mixed} - The attribute's value. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function(element, name) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError(`Graph.${method}: cannot find this type of edges in your ${this.type} graph.`); if (arguments.length > 2) { if (this.multi) throw new UsageGraphError(`Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`); const source = '' + element, target = '' + name; name = arguments[2]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`); } else { element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" edge in the graph.`); } if (type !== 'mixed' && data.undirected !== (type === 'undirected')) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" ${type} edge in the graph.`); return data.attributes[name]; }; } /** * Attach an attributes getter method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachAttributesGetter(Class, method, type) { /** * Retrieves all the target element's attributes. * * Arity 2: * @param {any} element - Target element. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * * @return {object} - The element's attributes. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function(element) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError(`Graph.${method}: cannot find this type of edges in your ${this.type} graph.`); if (arguments.length > 1) { if (this.multi) throw new UsageGraphError(`Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`); const source = '' + element, target = '' + arguments[1]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`); } else { element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" edge in the graph.`); } if (type !== 'mixed' && data.undirected !== (type === 'undirected')) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" ${type} edge in the graph.`); return data.attributes; }; } /** * Attach an attribute checker method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachAttributeChecker(Class, method, type) { /** * Checks whether the desired attribute is set for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {string} name - Attribute's name. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {string} name - Attribute's name. * * @return {boolean} * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function(element, name) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError(`Graph.${method}: cannot find this type of edges in your ${this.type} graph.`); if (arguments.length > 2) { if (this.multi) throw new UsageGraphError(`Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`); const source = '' + element, target = '' + name; name = arguments[2]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`); } else { element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" edge in the graph.`); } if (type !== 'mixed' && data.undirected !== (type === 'undirected')) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" ${type} edge in the graph.`); return data.attributes.hasOwnProperty(name); }; } /** * Attach an attribute setter method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachAttributeSetter(Class, method, type) { /** * Set the desired attribute for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {string} name - Attribute's name. * @param {mixed} value - New attribute value. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {string} name - Attribute's name. * @param {mixed} value - New attribute value. * * @return {Graph} - Returns itself for chaining. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function(element, name, value) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError(`Graph.${method}: cannot find this type of edges in your ${this.type} graph.`); if (arguments.length > 3) { if (this.multi) throw new UsageGraphError(`Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`); const source = '' + element, target = '' + name; name = arguments[2]; value = arguments[3]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`); } else { element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" edge in the graph.`); } if (type !== 'mixed' && data.undirected !== (type === 'undirected')) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" ${type} edge in the graph.`); data.attributes[name] = value; // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'set', attributes: data.attributes, name }); return this; }; } /** * Attach an attribute updater method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachAttributeUpdater(Class, method, type) { /** * Update the desired attribute for the given element (node or edge) using * the provided function. * * Arity 2: * @param {any} element - Target element. * @param {string} name - Attribute's name. * @param {function} updater - Updater function. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {string} name - Attribute's name. * @param {function} updater - Updater function. * * @return {Graph} - Returns itself for chaining. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function(element, name, updater) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError(`Graph.${method}: cannot find this type of edges in your ${this.type} graph.`); if (arguments.length > 3) { if (this.multi) throw new UsageGraphError(`Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`); const source = '' + element, target = '' + name; name = arguments[2]; updater = arguments[3]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`); } else { element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" edge in the graph.`); } if (typeof updater !== 'function') throw new InvalidArgumentsGraphError(`Graph.${method}: updater should be a function.`); if (type !== 'mixed' && data.undirected !== (type === 'undirected')) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" ${type} edge in the graph.`); data.attributes[name] = updater(data.attributes[name]); // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'set', attributes: data.attributes, name }); return this; }; } /** * Attach an attribute remover method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachAttributeRemover(Class, method, type) { /** * Remove the desired attribute for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {string} name - Attribute's name. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {string} name - Attribute's name. * * @return {Graph} - Returns itself for chaining. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function(element, name) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError(`Graph.${method}: cannot find this type of edges in your ${this.type} graph.`); if (arguments.length > 2) { if (this.multi) throw new UsageGraphError(`Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`); const source = '' + element, target = '' + name; name = arguments[2]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`); } else { element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" edge in the graph.`); } if (type !== 'mixed' && data.undirected !== (type === 'undirected')) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" ${type} edge in the graph.`); delete data.attributes[name]; // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'remove', attributes: data.attributes, name }); return this; }; } /** * Attach an attribute replacer method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachAttributesReplacer(Class, method, type) { /** * Replace the attributes for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {object} attributes - New attributes. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {object} attributes - New attributes. * * @return {Graph} - Returns itself for chaining. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function(element, attributes) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError(`Graph.${method}: cannot find this type of edges in your ${this.type} graph.`); if (arguments.length > 2) { if (this.multi) throw new UsageGraphError(`Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`); const source = '' + element, target = '' + attributes; attributes = arguments[2]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`); } else { element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" edge in the graph.`); } if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError(`Graph.${method}: provided attributes are not a plain object.`); if (type !== 'mixed' && data.undirected !== (type === 'undirected')) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" ${type} edge in the graph.`); data.attributes = attributes; // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'replace', attributes: data.attributes }); return this; }; } /** * Attach an attribute merger method onto the provided class. * * @param {function} Class - Target class. * @param {string} method - Method name. * @param {string} type - Type of the edge to find. */ function attachAttributesMerger(Class, method, type) { /** * Replace the attributes for the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {object} attributes - Attributes to merge. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @param {object} attributes - Attributes to merge. * * @return {Graph} - Returns itself for chaining. * * @throws {Error} - Will throw if too many arguments are provided. * @throws {Error} - Will throw if any of the elements is not found. */ Class.prototype[method] = function(element, attributes) { let data; if (this.type !== 'mixed' && type !== 'mixed' && type !== this.type) throw new UsageGraphError(`Graph.${method}: cannot find this type of edges in your ${this.type} graph.`); if (arguments.length > 2) { if (this.multi) throw new UsageGraphError(`Graph.${method}: cannot use a {source,target} combo when asking about an edge's attributes in a MultiGraph since we cannot infer the one you want information about.`); const source = '' + element, target = '' + attributes; attributes = arguments[2]; data = getMatchingEdge(this, source, target, type); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find an edge for the given path ("${source}" - "${target}").`); } else { element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" edge in the graph.`); } if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError(`Graph.${method}: provided attributes are not a plain object.`); if (type !== 'mixed' && data.undirected !== (type === 'undirected')) throw new NotFoundGraphError(`Graph.${method}: could not find the "${element}" ${type} edge in the graph.`); assign(data.attributes, attributes); // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'merge', attributes: data.attributes, data: attributes }); return this; }; } /** * List of methods to attach. */ const ATTRIBUTES_METHODS = [ { name: element => `get${element}Attribute`, attacher: attachAttributeGetter }, { name: element => `get${element}Attributes`, attacher: attachAttributesGetter }, { name: element => `has${element}Attribute`, attacher: attachAttributeChecker }, { name: element => `set${element}Attribute`, attacher: attachAttributeSetter }, { name: element => `update${element}Attribute`, attacher: attachAttributeUpdater }, { name: element => `remove${element}Attribute`, attacher: attachAttributeRemover }, { name: element => `replace${element}Attributes`, attacher: attachAttributesReplacer }, { name: element => `merge${element}Attributes`, attacher: attachAttributesMerger } ]; /** * Attach every attributes-related methods to a Graph class. * * @param {function} Graph - Target class. */ function attachAttributesMethods(Graph) { ATTRIBUTES_METHODS.forEach(function({name, attacher}) { // For edges attacher( Graph, name('Edge'), 'mixed' ); // For directed edges attacher( Graph, name('DirectedEdge'), 'directed' ); // For undirected edges attacher( Graph, name('UndirectedEdge'), 'undirected' ); }); } /** * Graphology Edge Iteration * ========================== * * Attaching some methods to the Graph class to be able to iterate over a * graph's edges. */ /** * Definitions. */ const EDGES_ITERATION = [ { name: 'edges', type: 'mixed' }, { name: 'inEdges', type: 'directed', direction: 'in' }, { name: 'outEdges', type: 'directed', direction: 'out' }, { name: 'inboundEdges', type: 'mixed', direction: 'in' }, { name: 'outboundEdges', type: 'mixed', direction: 'out' }, { name: 'directedEdges', type: 'directed' }, { name: 'undirectedEdges', type: 'undirected' } ]; /** * Function collecting edges from the given object. * * @param {array} edges - Edges array to populate. * @param {object} object - Target object. * @return {array} - The found edges. */ function collectSimple(edges, object) { for (const k in object) edges.push(object[k].key); } function collectMulti(edges, object) { for (const k in object) object[k].forEach(edgeData => edges.push(edgeData.key)); } /** * Function iterating over edges from the given object using a callback. * * @param {object} object - Target object. * @param {function} callback - Function to call. */ function forEachSimple(object, callback, avoid) { for (const k in object) { if (k === avoid) continue; const edgeData = object[k]; callback( edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected, edgeData.generatedKey ); } } function forEachMulti(object, callback, avoid) { for (const k in object) { if (k === avoid) continue; object[k].forEach(edgeData => callback( edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected, edgeData.generatedKey )); } } /** * Function iterating over edges from the given object using a callback until * the return value of the callback is truthy. * * @param {object} object - Target object. * @param {function} callback - Function to call. */ function forEachSimpleUntil(object, callback, avoid) { let shouldBreak = false; for (const k in object) { if (k === avoid) continue; const edgeData = object[k]; shouldBreak = callback( edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected, edgeData.generatedKey ); if (shouldBreak) return true; } return false; } function forEachMultiUntil(object, callback, avoid) { let iterator, step, edgeData, source, target; let shouldBreak = false; for (const k in object) { if (k === avoid) continue; iterator = object[k].values(); while ((step = iterator.next(), step.done !== true)) { edgeData = step.value; source = edgeData.source; target = edgeData.target; shouldBreak = callback( edgeData.key, edgeData.attributes, source.key, target.key, source.attributes, target.attributes, edgeData.undirected, edgeData.generatedKey ); if (shouldBreak) return true; } } return false; } /** * Function returning an iterator over edges from the given object. * * @param {object} object - Target object. * @return {Iterator} */ function createIterator(object, avoid) { const keys = Object.keys(object), l = keys.length; let inner = null, i = 0; return new Iterator(function next() { let edgeData; if (inner) { const step = inner.next(); if (step.done) { inner = null; i++; return next(); } edgeData = step.value; } else { if (i >= l) return {done: true}; const k = keys[i]; if (k === avoid) { i++; return next(); } edgeData = object[k]; if (edgeData instanceof Set) { inner = edgeData.values(); return next(); } i++; } return { done: false, value: [ edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes ] }; }); } /** * Function collecting edges from the given object at given key. * * @param {array} edges - Edges array to populate. * @param {object} object - Target object. * @param {mixed} k - Neighbor key. * @return {array} - The found edges. */ function collectForKeySimple(edges, object, k) { const edgeData = object[k]; if (!edgeData) return; edges.push(edgeData.key); } function collectForKeyMulti(edges, object, k) { const edgesData = object[k]; if (!edgesData) return; edgesData.forEach(edgeData => edges.push(edgeData.key)); } /** * Function iterating over the egdes from the object at given key using * a callback. * * @param {object} object - Target object. * @param {mixed} k - Neighbor key. * @param {function} callback - Callback to use. */ function forEachForKeySimple(object, k, callback) { const edgeData = object[k]; if (!edgeData) return; const sourceData = edgeData.source; const targetData = edgeData.target; callback( edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected, edgeData.generatedKey ); } function forEachForKeyMulti(object, k, callback) { const edgesData = object[k]; if (!edgesData) return; edgesData.forEach(edgeData => callback( edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected, edgeData.generatedKey )); } /** * Function iterating over the egdes from the object at given key using * a callback until it returns a truthy value to stop iteration. * * @param {object} object - Target object. * @param {mixed} k - Neighbor key. * @param {function} callback - Callback to use. */ function forEachForKeySimpleUntil(object, k, callback) { const edgeData = object[k]; if (!edgeData) return false; const sourceData = edgeData.source; const targetData = edgeData.target; return callback( edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected, edgeData.generatedKey ); } function forEachForKeyMultiUntil(object, k, callback) { const edgesData = object[k]; if (!edgesData) return false; let shouldBreak = false; const iterator = edgesData.values(); let step, edgeData; while ((step = iterator.next(), step.done !== true)) { edgeData = step.value; shouldBreak = callback( edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected, edgeData.generatedKey ); if (shouldBreak) return true; } return false; } /** * Function returning an iterator over the egdes from the object at given key. * * @param {object} object - Target object. * @param {mixed} k - Neighbor key. * @return {Iterator} */ function createIteratorForKey(object, k) { const v = object[k]; if (v instanceof Set) { const iterator = v.values(); return new Iterator(function() { const step = iterator.next(); if (step.done) return step; const edgeData = step.value; return { done: false, value: [ edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes ] }; }); } return Iterator.of([ v.key, v.attributes, v.source.key, v.target.key, v.source.attributes, v.target.attributes ]); } /** * Function creating an array of edges for the given type. * * @param {Graph} graph - Target Graph instance. * @param {string} type - Type of edges to retrieve. * @return {array} - Array of edges. */ function createEdgeArray(graph, type) { if (graph.size === 0) return []; if (type === 'mixed' || type === graph.type) { if (typeof Array.from === 'function') return Array.from(graph._edges.keys()); return take(graph._edges.keys(), graph._edges.size); } const size = type === 'undirected' ? graph.undirectedSize : graph.directedSize; const list = new Array(size), mask = type === 'undirected'; const iterator = graph._edges.values(); let i = 0; let step, data; while ((step = iterator.next(), step.done !== true)) { data = step.value; if (data.undirected === mask) list[i++] = data.key; } return list; } /** * Function iterating over a graph's edges using a callback. * * @param {Graph} graph - Target Graph instance. * @param {string} type - Type of edges to retrieve. * @param {function} callback - Function to call. */ function forEachEdge(graph, type, callback) { if (graph.size === 0) return; const shouldFilter = type !== 'mixed' && type !== graph.type; const mask = type === 'undirected'; let step, data; const iterator = graph._edges.values(); while ((step = iterator.next(), step.done !== true)) { data = step.value; if (shouldFilter && data.undirected !== mask) continue; const {key, attributes, source, target} = data; callback( key, attributes, source.key, target.key, source.attributes, target.attributes, data.undirected, data.generatedKey ); } } /** * Function iterating over a graph's edges using a callback until it returns * a truthy value to stop iteration. * * @param {Graph} graph - Target Graph instance. * @param {string} type - Type of edges to retrieve. * @param {function} callback - Function to call. */ function forEachEdgeUntil(graph, type, callback) { if (graph.size === 0) return false; const shouldFilter = type !== 'mixed' && type !== graph.type; const mask = type === 'undirected'; let step, data; let shouldBreak = false; const iterator = graph._edges.values(); while ((step = iterator.next(), step.done !== true)) { data = step.value; if (shouldFilter && data.undirected !== mask) continue; const {key, attributes, source, target} = data; shouldBreak = callback( key, attributes, source.key, target.key, source.attributes, target.attributes, data.undirected, data.generatedKey ); if (shouldBreak) return true; } return false; } /** * Function creating an iterator of edges for the given type. * * @param {Graph} graph - Target Graph instance. * @param {string} type - Type of edges to retrieve. * @return {Iterator} */ function createEdgeIterator(graph, type) { if (graph.size === 0) return Iterator.empty(); const shouldFilter = type !== 'mixed' && type !== graph.type; const mask = type === 'undirected'; const iterator = graph._edges.values(); return new Iterator(function next() { let step, data; // eslint-disable-next-line no-constant-condition while (true) { step = iterator.next(); if (step.done) return step; data = step.value; if (shouldFilter && data.undirected !== mask) continue; break; } const value = [ data.key, data.attributes, data.source.key, data.target.key, data.source.attributes, data.target.attributes ]; return {value, done: false}; }); } /** * Function creating an array of edges for the given type & the given node. * * @param {boolean} multi - Whether the graph is multi or not. * @param {string} type - Type of edges to retrieve. * @param {string} direction - In or out? * @param {any} nodeData - Target node's data. * @return {array} - Array of edges. */ function createEdgeArrayForNode(multi, type, direction, nodeData) { const edges = []; const fn = multi ? collectMulti : collectSimple; if (type !== 'undirected') { if (direction !== 'out') fn(edges, nodeData.in); if (direction !== 'in') fn(edges, nodeData.out); // Handling self loop edge case if (!direction && nodeData.directedSelfLoops > 0) edges.splice(edges.lastIndexOf(nodeData.key), 1); } if (type !== 'directed') { fn(edges, nodeData.undirected); } return edges; } /** * Function iterating over a node's edges using a callback. * * @param {boolean} multi - Whether the graph is multi or not. * @param {string} type - Type of edges to retrieve. * @param {string} direction - In or out? * @param {any} nodeData - Target node's data. * @param {function} callback - Function to call. */ function forEachEdgeForNode(multi, type, direction, nodeData, callback) { const fn = multi ? forEachMulti : forEachSimple; if (type !== 'undirected') { if (direction !== 'out') fn(nodeData.in, callback); if (direction !== 'in') fn(nodeData.out, callback, !direction ? nodeData.key : null); } if (type !== 'directed') { fn(nodeData.undirected, callback); } } /** * Function iterating over a node's edges using a callback until it returns * a truthy value to stop iteration. * * @param {boolean} multi - Whether the graph is multi or not. * @param {string} type - Type of edges to retrieve. * @param {string} direction - In or out? * @param {any} nodeData - Target node's data. * @param {function} callback - Function to call. */ function forEachEdgeForNodeUntil(multi, type, direction, nodeData, callback) { const fn = multi ? forEachMultiUntil : forEachSimpleUntil; let shouldBreak = false; if (type !== 'undirected') { if (direction !== 'out') { shouldBreak = fn(nodeData.in, callback); if (shouldBreak) return true; } if (direction !== 'in') { shouldBreak = fn(nodeData.out, callback, !direction ? nodeData.key : null); if (shouldBreak) return true; } } if (type !== 'directed') { shouldBreak = fn(nodeData.undirected, callback); if (shouldBreak) return true; } return false; } /** * Function iterating over a node's edges using a callback. * * @param {string} type - Type of edges to retrieve. * @param {string} direction - In or out? * @param {any} nodeData - Target node's data. * @return {Iterator} */ function createEdgeIteratorForNode(type, direction, nodeData) { let iterator = Iterator.empty(); if (type !== 'undirected') { if (direction !== 'out' && typeof nodeData.in !== 'undefined') iterator = chain(iterator, createIterator(nodeData.in)); if (direction !== 'in' && typeof nodeData.out !== 'undefined') iterator = chain(iterator, createIterator(nodeData.out, !direction ? nodeData.key : null)); } if (type !== 'directed' && typeof nodeData.undirected !== 'undefined') { iterator = chain(iterator, createIterator(nodeData.undirected)); } return iterator; } /** * Function creating an array of edges for the given path. * * @param {string} type - Type of edges to retrieve. * @param {boolean} multi - Whether the graph is multi. * @param {string} direction - In or out? * @param {NodeData} sourceData - Source node's data. * @param {any} target - Target node. * @return {array} - Array of edges. */ function createEdgeArrayForPath(type, multi, direction, sourceData, target) { const fn = multi ? collectForKeyMulti : collectForKeySimple; const edges = []; if (type !== 'undirected') { if (typeof sourceData.in !== 'undefined' && direction !== 'out') fn(edges, sourceData.in, target); if (typeof sourceData.out !== 'undefined' && direction !== 'in') fn(edges, sourceData.out, target); // Handling self loop edge case if (!direction && sourceData.directedSelfLoops > 0) edges.splice(edges.lastIndexOf(sourceData.key), 1); } if (type !== 'directed') { if (typeof sourceData.undirected !== 'undefined') fn(edges, sourceData.undirected, target); } return edges; } /** * Function iterating over edges for the given path using a callback. * * @param {string} type - Type of edges to retrieve. * @param {boolean} multi - Whether the graph is multi. * @param {string} direction - In or out? * @param {NodeData} sourceData - Source node's data. * @param {string} target - Target node. * @param {function} callback - Function to call. */ function forEachEdgeForPath(type, multi, direction, sourceData, target, callback) { const fn = multi ? forEachForKeyMulti : forEachForKeySimple; if (type !== 'undirected') { if (typeof sourceData.in !== 'undefined' && direction !== 'out') fn(sourceData.in, target, callback); if (sourceData.key !== target) if (typeof sourceData.o