eventemitter3-graphology
Version:
A robust and multipurpose Graph object for JavaScript.
1,941 lines (1,638 loc) • 178 kB
JavaScript
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 {