UNPKG

eventemitter3-graphology

Version:

A robust and multipurpose Graph object for JavaScript.

1,941 lines (1,638 loc) 178 kB
import { EventEmitter } from 'eventemitter3'; /** * 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 plain object. * * @param {mixed} value - Target value. * @return {boolean} */ function isPlainObject(value) { // NOTE: as per https://github.com/graphology/graphology/issues/149 // this function has been loosened not to reject object instances // coming from other JavaScript contexts. It has also been chosen // not to improve it to avoid obvious false positives and avoid // taking a performance hit. People should really use TypeScript // if they want to avoid feeding subtly irrelvant attribute objects. return typeof value === 'object' && value !== null; } /** * 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 incrementalIdStartingFromRandomByte() { let i = Math.floor(Math.random() * 256) & 0xff; return () => { return i++; }; } /** * Chains multiple iterators into a single iterator. * * @param {...Iterator} iterables * @returns {Iterator} */ function chain() { const iterables = arguments; let current = null; let i = -1; return { [Symbol.iterator]() { return this; }, next() { let step = null; do { if (current === null) { i++; if (i >= iterables.length) return {done: true}; current = iterables[i][Symbol.iterator](); } step = current.next(); if (step.done) { current = null; continue; } break; // eslint-disable-next-line no-constant-condition } while (true); return step; } }; } function emptyIterator() { return { [Symbol.iterator]() { return this; }, next() { return {done: true}; } }; } /** * 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) { super(); this.name = 'GraphError'; this.message = message; } } class InvalidArgumentsGraphError extends GraphError { constructor(message) { super(message); 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) { super(message); 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) { super(message); 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; this.clear(); } MixedNodeData.prototype.clear = function () { // Degrees this.inDegree = 0; this.outDegree = 0; this.undirectedDegree = 0; this.undirectedLoops = 0; this.directedLoops = 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; this.clear(); } DirectedNodeData.prototype.clear = function () { // Degrees this.inDegree = 0; this.outDegree = 0; this.directedLoops = 0; // Indices this.in = {}; this.out = {}; }; /** * 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; this.clear(); } UndirectedNodeData.prototype.clear = function () { // Degrees this.undirectedDegree = 0; this.undirectedLoops = 0; // Indices this.undirected = {}; }; /** * EdgeData class. * * @constructor * @param {boolean} undirected - Whether the edge is undirected. * @param {string} string - The edge's key. * @param {string} source - Source of the edge. * @param {string} target - Target of the edge. * @param {object} attributes - Edge's attributes. */ function EdgeData(undirected, key, source, target, attributes) { // Attributes this.key = key; this.attributes = attributes; this.undirected = undirected; // Extremities this.source = source; this.target = target; } EdgeData.prototype.attach = function () { let outKey = 'out'; let inKey = 'in'; if (this.undirected) outKey = inKey = 'undirected'; const source = this.source.key; const target = this.target.key; // Handling source this.source[outKey][target] = this; if (this.undirected && source === target) return; // Handling target this.target[inKey][source] = this; }; EdgeData.prototype.attachMulti = function () { let outKey = 'out'; let inKey = 'in'; const source = this.source.key; const target = this.target.key; if (this.undirected) outKey = inKey = 'undirected'; // Handling source const adj = this.source[outKey]; const head = adj[target]; if (typeof head === 'undefined') { adj[target] = this; // Self-loop optimization if (!(this.undirected && source === target)) { // Handling target this.target[inKey][source] = this; } return; } // Prepending to doubly-linked list head.previous = this; this.next = head; // Pointing to new head // NOTE: use mutating swap later to avoid lookup? adj[target] = this; this.target[inKey][source] = this; }; EdgeData.prototype.detach = function () { const source = this.source.key; const target = this.target.key; let outKey = 'out'; let inKey = 'in'; if (this.undirected) outKey = inKey = 'undirected'; delete this.source[outKey][target]; // No-op delete in case of undirected self-loop delete this.target[inKey][source]; }; EdgeData.prototype.detachMulti = function () { const source = this.source.key; const target = this.target.key; let outKey = 'out'; let inKey = 'in'; if (this.undirected) outKey = inKey = 'undirected'; // Deleting from doubly-linked list if (this.previous === undefined) { // We are dealing with the head // Should we delete the adjacency entry because it is now empty? if (this.next === undefined) { delete this.source[outKey][target]; // No-op delete in case of undirected self-loop delete this.target[inKey][source]; } else { // Detaching this.next.previous = undefined; // NOTE: could avoid the lookups by creating a #.become mutating method this.source[outKey][target] = this.next; // No-op delete in case of undirected self-loop this.target[inKey][source] = this.next; } } else { // We are dealing with another list node this.previous.next = this.next; // If not last if (this.next !== undefined) { this.next.previous = this.previous; } } }; /** * Graphology Node Attributes methods * =================================== */ const NODE = 0; const SOURCE = 1; const TARGET = 2; const OPPOSITE = 3; function findRelevantNodeData( graph, method, mode, nodeOrEdge, nameOrEdge, add1, add2 ) { let nodeData, edgeData, arg1, arg2; nodeOrEdge = '' + nodeOrEdge; if (mode === NODE) { nodeData = graph._nodes.get(nodeOrEdge); if (!nodeData) throw new NotFoundGraphError( `Graph.${method}: could not find the "${nodeOrEdge}" node in the graph.` ); arg1 = nameOrEdge; arg2 = add1; } else if (mode === OPPOSITE) { nameOrEdge = '' + nameOrEdge; edgeData = graph._edges.get(nameOrEdge); if (!edgeData) throw new NotFoundGraphError( `Graph.${method}: could not find the "${nameOrEdge}" edge in the graph.` ); const source = edgeData.source.key; const target = edgeData.target.key; if (nodeOrEdge === source) { nodeData = edgeData.target; } else if (nodeOrEdge === target) { nodeData = edgeData.source; } else { throw new NotFoundGraphError( `Graph.${method}: the "${nodeOrEdge}" node is not attached to the "${nameOrEdge}" edge (${source}, ${target}).` ); } arg1 = add1; arg2 = add2; } else { edgeData = graph._edges.get(nodeOrEdge); if (!edgeData) throw new NotFoundGraphError( `Graph.${method}: could not find the "${nodeOrEdge}" edge in the graph.` ); if (mode === SOURCE) { nodeData = edgeData.source; } else { nodeData = edgeData.target; } arg1 = nameOrEdge; arg2 = add1; } return [nodeData, arg1, arg2]; } function attachNodeAttributeGetter(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, name] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); return data.attributes[name]; }; } function attachNodeAttributesGetter(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge) { const [data] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge ); return data.attributes; }; } function attachNodeAttributeChecker(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, name] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); return data.attributes.hasOwnProperty(name); }; } function attachNodeAttributeSetter(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) { const [data, name, value] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1, add2 ); data.attributes[name] = value; // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'set', attributes: data.attributes, name }); return this; }; } function attachNodeAttributeUpdater(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1, add2) { const [data, name, updater] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1, add2 ); if (typeof updater !== 'function') throw new InvalidArgumentsGraphError( `Graph.${method}: updater should be a function.` ); const attributes = data.attributes; const value = updater(attributes[name]); attributes[name] = value; // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'set', attributes: data.attributes, name }); return this; }; } function attachNodeAttributeRemover(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, name] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); delete data.attributes[name]; // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'remove', attributes: data.attributes, name }); return this; }; } function attachNodeAttributesReplacer(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, attributes] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError( `Graph.${method}: provided attributes are not a plain object.` ); data.attributes = attributes; // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'replace', attributes: data.attributes }); return this; }; } function attachNodeAttributesMerger(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, attributes] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); if (!isPlainObject(attributes)) throw new InvalidArgumentsGraphError( `Graph.${method}: provided attributes are not a plain object.` ); assign(data.attributes, attributes); // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'merge', attributes: data.attributes, data: attributes }); return this; }; } function attachNodeAttributesUpdater(Class, method, mode) { Class.prototype[method] = function (nodeOrEdge, nameOrEdge, add1) { const [data, updater] = findRelevantNodeData( this, method, mode, nodeOrEdge, nameOrEdge, add1 ); if (typeof updater !== 'function') throw new InvalidArgumentsGraphError( `Graph.${method}: provided updater is not a function.` ); data.attributes = updater(data.attributes); // Emitting this.emit('nodeAttributesUpdated', { key: data.key, type: 'update', attributes: data.attributes }); return this; }; } /** * List of methods to attach. */ const NODE_ATTRIBUTES_METHODS = [ { name: element => `get${element}Attribute`, attacher: attachNodeAttributeGetter }, { name: element => `get${element}Attributes`, attacher: attachNodeAttributesGetter }, { name: element => `has${element}Attribute`, attacher: attachNodeAttributeChecker }, { name: element => `set${element}Attribute`, attacher: attachNodeAttributeSetter }, { name: element => `update${element}Attribute`, attacher: attachNodeAttributeUpdater }, { name: element => `remove${element}Attribute`, attacher: attachNodeAttributeRemover }, { name: element => `replace${element}Attributes`, attacher: attachNodeAttributesReplacer }, { name: element => `merge${element}Attributes`, attacher: attachNodeAttributesMerger }, { name: element => `update${element}Attributes`, attacher: attachNodeAttributesUpdater } ]; /** * Attach every attributes-related methods to a Graph class. * * @param {function} Graph - Target class. */ function attachNodeAttributesMethods(Graph) { NODE_ATTRIBUTES_METHODS.forEach(function ({name, attacher}) { // For nodes attacher(Graph, name('Node'), NODE); // For sources attacher(Graph, name('Source'), SOURCE); // For targets attacher(Graph, name('Target'), TARGET); // For opposites attacher(Graph, name('Opposite'), OPPOSITE); }); } /** * Graphology Edge Attributes methods * =================================== */ /** * 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 attachEdgeAttributeGetter(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; const 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 { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" 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 attachEdgeAttributesGetter(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 { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" 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 attachEdgeAttributeChecker(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; const 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 { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" 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 attachEdgeAttributeSetter(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; const 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 { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" 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 attachEdgeAttributeUpdater(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; const 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 { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); 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.` ); 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 attachEdgeAttributeRemover(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; const 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 { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); element = '' + element; data = this._edges.get(element); if (!data) throw new NotFoundGraphError( `Graph.${method}: could not find the "${element}" 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 attachEdgeAttributesReplacer(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 { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); 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.` ); 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 attachEdgeAttributesMerger(Class, method, type) { /** * Merge 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 { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); 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.` ); assign(data.attributes, attributes); // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'merge', attributes: data.attributes, data: attributes }); 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 attachEdgeAttributesUpdater(Class, method, type) { /** * Update the attributes of the given element (node or edge). * * Arity 2: * @param {any} element - Target element. * @param {function} updater - Updater function. * * Arity 3 (only for edges): * @param {any} source - Source element. * @param {any} target - Target element. * @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, 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 > 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 = '' + updater; updater = 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 { if (type !== 'mixed') throw new UsageGraphError( `Graph.${method}: calling this method with only a key (vs. a source and target) does not make sense since an edge with this key could have the other type.` ); 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}: provided updater is not a function.` ); data.attributes = updater(data.attributes); // Emitting this.emit('edgeAttributesUpdated', { key: data.key, type: 'update', attributes: data.attributes }); return this; }; } /** * List of methods to attach. */ const EDGE_ATTRIBUTES_METHODS = [ { name: element => `get${element}Attribute`, attacher: attachEdgeAttributeGetter }, { name: element => `get${element}Attributes`, attacher: attachEdgeAttributesGetter }, { name: element => `has${element}Attribute`, attacher: attachEdgeAttributeChecker }, { name: element => `set${element}Attribute`, attacher: attachEdgeAttributeSetter }, { name: element => `update${element}Attribute`, attacher: attachEdgeAttributeUpdater }, { name: element => `remove${element}Attribute`, attacher: attachEdgeAttributeRemover }, { name: element => `replace${element}Attributes`, attacher: attachEdgeAttributesReplacer }, { name: element => `merge${element}Attributes`, attacher: attachEdgeAttributesMerger }, { name: element => `update${element}Attributes`, attacher: attachEdgeAttributesUpdater } ]; /** * Attach every attributes-related methods to a Graph class. * * @param {function} Graph - Target class. */ function attachEdgeAttributesMethods(Graph) { EDGE_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 iterating over edges from the given object to match one of them. * * @param {object} object - Target object. * @param {function} callback - Function to call. */ function forEachSimple(breakable, 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 ); if (breakable && shouldBreak) return edgeData.key; } return; } function forEachMulti(breakable, object, callback, avoid) { let edgeData, source, target; let shouldBreak = false; for (const k in object) { if (k === avoid) continue; edgeData = object[k]; do { source = edgeData.source; target = edgeData.target; shouldBreak = callback( edgeData.key, edgeData.attributes, source.key, target.key, source.attributes, target.attributes, edgeData.undirected ); if (breakable && shouldBreak) return edgeData.key; edgeData = edgeData.next; } while (edgeData !== undefined); } return; } /** * 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); const l = keys.length; let edgeData; let i = 0; return { [Symbol.iterator]() { return this; }, next() { do { if (!edgeData) { if (i >= l) return {done: true}; const k = keys[i++]; if (k === avoid) { edgeData = undefined; continue; } edgeData = object[k]; } else { edgeData = edgeData.next; } } while (!edgeData); return { done: false, value: { edge: edgeData.key, attributes: edgeData.attributes, source: edgeData.source.key, target: edgeData.target.key, sourceAttributes: edgeData.source.attributes, targetAttributes: edgeData.target.attributes, undirected: edgeData.undirected } }; } }; } /** * Function iterating over the egdes from the object at given key to match * one of them. * * @param {object} object - Target object. * @param {mixed} k - Neighbor key. * @param {function} callback - Callback to use. */ function forEachForKeySimple(breakable, object, k, callback) { const edgeData = object[k]; if (!edgeData) return; const sourceData = edgeData.source; const targetData = edgeData.target; if ( callback( edgeData.key, edgeData.attributes, sourceData.key, targetData.key, sourceData.attributes, targetData.attributes, edgeData.undirected ) && breakable ) return edgeData.key; } function forEachForKeyMulti(breakable, object, k, callback) { let edgeData = object[k]; if (!edgeData) return; let shouldBreak = false; do { shouldBreak = callback( edgeData.key, edgeData.attributes, edgeData.source.key, edgeData.target.key, edgeData.source.attributes, edgeData.target.attributes, edgeData.undirected ); if (breakable && shouldBreak) return edgeData.key; edgeData = edgeData.next; } while (edgeData !== undefined); return; } /** * 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) { let edgeData = object[k]; if (edgeData.next !== undefined) { return { [Symbol.iterator]() { return this; }, next() { if (!edgeData) return {done: true}; const value = { edge: edgeData.key, attributes: edgeData.attributes, source: edgeData.source.key, target: edgeData.target.key, sourceAttributes: edgeData.source.attributes, targetAttributes: edgeData.target.attributes, undirected: edgeData.undirected }; edgeData = edgeData.next; return { done: false, value }; } }; } let done = false; return { [Symbol.iterator]() { return this; }, next() { if (done === true) return {done: true}; done = true; return { done: false, value: { edge: edgeData.key, attributes: edgeData.attributes, source: edgeData.source.key, target: edgeData.target.key, sourceAttributes: edgeData.source.attributes, targetAttributes: edgeData.target.attributes, undirected: edgeData.undirected } }; } }; } /** * 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) { return Array.from(graph._edges.keys()); } 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 to match one of * them. * * @param {Graph} graph - Target Graph instance. * @param {string} type - Type of edges to retrieve. * @param {function} callback - Function to call. */ function forEachEdge(breakable, graph, type, callback) { if (graph.size === 0) return; 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 {