@dagrejs/graphlib
Version:
A directed and undirected multi-graph library
697 lines (608 loc) • 17.2 kB
JavaScript
"use strict";
var DEFAULT_EDGE_NAME = "\x00";
var GRAPH_NODE = "\x00";
var EDGE_KEY_DELIM = "\x01";
// Implementation notes:
//
// * Node id query functions should return string ids for the nodes
// * Edge id query functions should return an "edgeObj", edge object, that is
// composed of enough information to uniquely identify an edge: {v, w, name}.
// * Internally we use an "edgeId", a stringified form of the edgeObj, to
// reference edges. This is because we need a performant way to look these
// edges up and, object properties, which have string keys, are the closest
// we're going to get to a performant hashtable in JavaScript.
class Graph {
_isDirected = true;
_isMultigraph = false;
_isCompound = false;
// Label for the graph itself
_label;
// Defaults to be set when creating a new node
_defaultNodeLabelFn = () => undefined;
// Defaults to be set when creating a new edge
_defaultEdgeLabelFn = () => undefined;
// v -> label
_nodes = {};
// v -> edgeObj
_in = {};
// u -> v -> Number
_preds = {};
// v -> edgeObj
_out = {};
// v -> w -> Number
_sucs = {};
// e -> edgeObj
_edgeObjs = {};
// e -> label
_edgeLabels = {};
/* Number of nodes in the graph. Should only be changed by the implementation. */
_nodeCount = 0;
/* Number of edges in the graph. Should only be changed by the implementation. */
_edgeCount = 0;
_parent;
_children;
constructor(opts) {
if (opts) {
this._isDirected = Object.hasOwn(opts, "directed") ? opts.directed : true;
this._isMultigraph = Object.hasOwn(opts, "multigraph") ? opts.multigraph : false;
this._isCompound = Object.hasOwn(opts, "compound") ? opts.compound : false;
}
if (this._isCompound) {
// v -> parent
this._parent = {};
// v -> children
this._children = {};
this._children[GRAPH_NODE] = {};
}
}
/* === Graph functions ========= */
/**
* Whether graph was created with 'directed' flag set to true or not.
*/
isDirected() {
return this._isDirected;
}
/**
* Whether graph was created with 'multigraph' flag set to true or not.
*/
isMultigraph() {
return this._isMultigraph;
}
/**
* Whether graph was created with 'compound' flag set to true or not.
*/
isCompound() {
return this._isCompound;
}
/**
* Sets the label of the graph.
*/
setGraph(label) {
this._label = label;
return this;
}
/**
* Gets the graph label.
*/
graph() {
return this._label;
}
/* === Node functions ========== */
/**
* Sets the default node label. If newDefault is a function, it will be
* invoked ach time when setting a label for a node. Otherwise, this label
* will be assigned as default label in case if no label was specified while
* setting a node.
* Complexity: O(1).
*/
setDefaultNodeLabel(newDefault) {
this._defaultNodeLabelFn = newDefault;
if (typeof newDefault !== 'function') {
this._defaultNodeLabelFn = () => newDefault;
}
return this;
}
/**
* Gets the number of nodes in the graph.
* Complexity: O(1).
*/
nodeCount() {
return this._nodeCount;
}
/**
* Gets all nodes of the graph. Note, the in case of compound graph subnodes are
* not included in list.
* Complexity: O(1).
*/
nodes() {
return Object.keys(this._nodes);
}
/**
* Gets list of nodes without in-edges.
* Complexity: O(|V|).
*/
sources() {
var self = this;
return this.nodes().filter(v => Object.keys(self._in[v]).length === 0);
}
/**
* Gets list of nodes without out-edges.
* Complexity: O(|V|).
*/
sinks() {
var self = this;
return this.nodes().filter(v => Object.keys(self._out[v]).length === 0);
}
/**
* Invokes setNode method for each node in names list.
* Complexity: O(|names|).
*/
setNodes(vs, value) {
var args = arguments;
var self = this;
vs.forEach(function(v) {
if (args.length > 1) {
self.setNode(v, value);
} else {
self.setNode(v);
}
});
return this;
}
/**
* Creates or updates the value for the node v in the graph. If label is supplied
* it is set as the value for the node. If label is not supplied and the node was
* created by this call then the default node label will be assigned.
* Complexity: O(1).
*/
setNode(v, value) {
if (Object.hasOwn(this._nodes, v)) {
if (arguments.length > 1) {
this._nodes[v] = value;
}
return this;
}
this._nodes[v] = arguments.length > 1 ? value : this._defaultNodeLabelFn(v);
if (this._isCompound) {
this._parent[v] = GRAPH_NODE;
this._children[v] = {};
this._children[GRAPH_NODE][v] = true;
}
this._in[v] = {};
this._preds[v] = {};
this._out[v] = {};
this._sucs[v] = {};
++this._nodeCount;
return this;
}
/**
* Gets the label of node with specified name.
* Complexity: O(|V|).
*/
node(v) {
return this._nodes[v];
}
/**
* Detects whether graph has a node with specified name or not.
*/
hasNode(v) {
return Object.hasOwn(this._nodes, v);
}
/**
* Remove the node with the name from the graph or do nothing if the node is not in
* the graph. If the node was removed this function also removes any incident
* edges.
* Complexity: O(1).
*/
removeNode(v) {
var self = this;
if (Object.hasOwn(this._nodes, v)) {
var removeEdge = e => self.removeEdge(self._edgeObjs[e]);
delete this._nodes[v];
if (this._isCompound) {
this._removeFromParentsChildList(v);
delete this._parent[v];
this.children(v).forEach(function(child) {
self.setParent(child);
});
delete this._children[v];
}
Object.keys(this._in[v]).forEach(removeEdge);
delete this._in[v];
delete this._preds[v];
Object.keys(this._out[v]).forEach(removeEdge);
delete this._out[v];
delete this._sucs[v];
--this._nodeCount;
}
return this;
}
/**
* Sets node p as a parent for node v if it is defined, or removes the
* parent for v if p is undefined. Method throws an exception in case of
* invoking it in context of noncompound graph.
* Average-case complexity: O(1).
*/
setParent(v, parent) {
if (!this._isCompound) {
throw new Error("Cannot set parent in a non-compound graph");
}
if (parent === undefined) {
parent = GRAPH_NODE;
} else {
// Coerce parent to string
parent += "";
for (var ancestor = parent; ancestor !== undefined; ancestor = this.parent(ancestor)) {
if (ancestor === v) {
throw new Error("Setting " + parent+ " as parent of " + v +
" would create a cycle");
}
}
this.setNode(parent);
}
this.setNode(v);
this._removeFromParentsChildList(v);
this._parent[v] = parent;
this._children[parent][v] = true;
return this;
}
_removeFromParentsChildList(v) {
delete this._children[this._parent[v]][v];
}
/**
* Gets parent node for node v.
* Complexity: O(1).
*/
parent(v) {
if (this._isCompound) {
var parent = this._parent[v];
if (parent !== GRAPH_NODE) {
return parent;
}
}
}
/**
* Gets list of direct children of node v.
* Complexity: O(1).
*/
children(v = GRAPH_NODE) {
if (this._isCompound) {
var children = this._children[v];
if (children) {
return Object.keys(children);
}
} else if (v === GRAPH_NODE) {
return this.nodes();
} else if (this.hasNode(v)) {
return [];
}
}
/**
* Return all nodes that are predecessors of the specified node or undefined if node v is not in
* the graph. Behavior is undefined for undirected graphs - use neighbors instead.
* Complexity: O(|V|).
*/
predecessors(v) {
var predsV = this._preds[v];
if (predsV) {
return Object.keys(predsV);
}
}
/**
* Return all nodes that are successors of the specified node or undefined if node v is not in
* the graph. Behavior is undefined for undirected graphs - use neighbors instead.
* Complexity: O(|V|).
*/
successors(v) {
var sucsV = this._sucs[v];
if (sucsV) {
return Object.keys(sucsV);
}
}
/**
* Return all nodes that are predecessors or successors of the specified node or undefined if
* node v is not in the graph.
* Complexity: O(|V|).
*/
neighbors(v) {
var preds = this.predecessors(v);
if (preds) {
const union = new Set(preds);
for (var succ of this.successors(v)) {
union.add(succ);
}
return Array.from(union.values());
}
}
isLeaf(v) {
var neighbors;
if (this.isDirected()) {
neighbors = this.successors(v);
} else {
neighbors = this.neighbors(v);
}
return neighbors.length === 0;
}
/**
* Creates new graph with nodes filtered via filter. Edges incident to rejected node
* are also removed. In case of compound graph, if parent is rejected by filter,
* than all its children are rejected too.
* Average-case complexity: O(|E|+|V|).
*/
filterNodes(filter) {
var copy = new this.constructor({
directed: this._isDirected,
multigraph: this._isMultigraph,
compound: this._isCompound
});
copy.setGraph(this.graph());
var self = this;
Object.entries(this._nodes).forEach(function([v, value]) {
if (filter(v)) {
copy.setNode(v, value);
}
});
Object.values(this._edgeObjs).forEach(function(e) {
if (copy.hasNode(e.v) && copy.hasNode(e.w)) {
copy.setEdge(e, self.edge(e));
}
});
var parents = {};
function findParent(v) {
var parent = self.parent(v);
if (parent === undefined || copy.hasNode(parent)) {
parents[v] = parent;
return parent;
} else if (parent in parents) {
return parents[parent];
} else {
return findParent(parent);
}
}
if (this._isCompound) {
copy.nodes().forEach(v => copy.setParent(v, findParent(v)));
}
return copy;
}
/* === Edge functions ========== */
/**
* Sets the default edge label or factory function. This label will be
* assigned as default label in case if no label was specified while setting
* an edge or this function will be invoked each time when setting an edge
* with no label specified and returned value * will be used as a label for edge.
* Complexity: O(1).
*/
setDefaultEdgeLabel(newDefault) {
this._defaultEdgeLabelFn = newDefault;
if (typeof newDefault !== 'function') {
this._defaultEdgeLabelFn = () => newDefault;
}
return this;
}
/**
* Gets the number of edges in the graph.
* Complexity: O(1).
*/
edgeCount() {
return this._edgeCount;
}
/**
* Gets edges of the graph. In case of compound graph subgraphs are not considered.
* Complexity: O(|E|).
*/
edges() {
return Object.values(this._edgeObjs);
}
/**
* Establish an edges path over the nodes in nodes list. If some edge is already
* exists, it will update its label, otherwise it will create an edge between pair
* of nodes with label provided or default label if no label provided.
* Complexity: O(|nodes|).
*/
setPath(vs, value) {
var self = this;
var args = arguments;
vs.reduce(function(v, w) {
if (args.length > 1) {
self.setEdge(v, w, value);
} else {
self.setEdge(v, w);
}
return w;
});
return this;
}
/**
* Creates or updates the label for the edge (v, w) with the optionally supplied
* name. If label is supplied it is set as the value for the edge. If label is not
* supplied and the edge was created by this call then the default edge label will
* be assigned. The name parameter is only useful with multigraphs.
*/
setEdge() {
var v, w, name, value;
var valueSpecified = false;
var arg0 = arguments[0];
if (typeof arg0 === "object" && arg0 !== null && "v" in arg0) {
v = arg0.v;
w = arg0.w;
name = arg0.name;
if (arguments.length === 2) {
value = arguments[1];
valueSpecified = true;
}
} else {
v = arg0;
w = arguments[1];
name = arguments[3];
if (arguments.length > 2) {
value = arguments[2];
valueSpecified = true;
}
}
v = "" + v;
w = "" + w;
if (name !== undefined) {
name = "" + name;
}
var e = edgeArgsToId(this._isDirected, v, w, name);
if (Object.hasOwn(this._edgeLabels, e)) {
if (valueSpecified) {
this._edgeLabels[e] = value;
}
return this;
}
if (name !== undefined && !this._isMultigraph) {
throw new Error("Cannot set a named edge when isMultigraph = false");
}
// It didn't exist, so we need to create it.
// First ensure the nodes exist.
this.setNode(v);
this.setNode(w);
this._edgeLabels[e] = valueSpecified ? value : this._defaultEdgeLabelFn(v, w, name);
var edgeObj = edgeArgsToObj(this._isDirected, v, w, name);
// Ensure we add undirected edges in a consistent way.
v = edgeObj.v;
w = edgeObj.w;
Object.freeze(edgeObj);
this._edgeObjs[e] = edgeObj;
incrementOrInitEntry(this._preds[w], v);
incrementOrInitEntry(this._sucs[v], w);
this._in[w][e] = edgeObj;
this._out[v][e] = edgeObj;
this._edgeCount++;
return this;
}
/**
* Gets the label for the specified edge.
* Complexity: O(1).
*/
edge(v, w, name) {
var e = (arguments.length === 1
? edgeObjToId(this._isDirected, arguments[0])
: edgeArgsToId(this._isDirected, v, w, name));
return this._edgeLabels[e];
}
/**
* Gets the label for the specified edge and converts it to an object.
* Complexity: O(1)
*/
edgeAsObj() {
const edge = this.edge(...arguments);
if (typeof edge !== "object") {
return {label: edge};
}
return edge;
}
/**
* Detects whether the graph contains specified edge or not. No subgraphs are considered.
* Complexity: O(1).
*/
hasEdge(v, w, name) {
var e = (arguments.length === 1
? edgeObjToId(this._isDirected, arguments[0])
: edgeArgsToId(this._isDirected, v, w, name));
return Object.hasOwn(this._edgeLabels, e);
}
/**
* Removes the specified edge from the graph. No subgraphs are considered.
* Complexity: O(1).
*/
removeEdge(v, w, name) {
var e = (arguments.length === 1
? edgeObjToId(this._isDirected, arguments[0])
: edgeArgsToId(this._isDirected, v, w, name));
var edge = this._edgeObjs[e];
if (edge) {
v = edge.v;
w = edge.w;
delete this._edgeLabels[e];
delete this._edgeObjs[e];
decrementOrRemoveEntry(this._preds[w], v);
decrementOrRemoveEntry(this._sucs[v], w);
delete this._in[w][e];
delete this._out[v][e];
this._edgeCount--;
}
return this;
}
/**
* Return all edges that point to the node v. Optionally filters those edges down to just those
* coming from node u. Behavior is undefined for undirected graphs - use nodeEdges instead.
* Complexity: O(|E|).
*/
inEdges(v, u) {
var inV = this._in[v];
if (inV) {
var edges = Object.values(inV);
if (!u) {
return edges;
}
return edges.filter(edge => edge.v === u);
}
}
/**
* Return all edges that are pointed at by node v. Optionally filters those edges down to just
* those point to w. Behavior is undefined for undirected graphs - use nodeEdges instead.
* Complexity: O(|E|).
*/
outEdges(v, w) {
var outV = this._out[v];
if (outV) {
var edges = Object.values(outV);
if (!w) {
return edges;
}
return edges.filter(edge => edge.w === w);
}
}
/**
* Returns all edges to or from node v regardless of direction. Optionally filters those edges
* down to just those between nodes v and w regardless of direction.
* Complexity: O(|E|).
*/
nodeEdges(v, w) {
var inEdges = this.inEdges(v, w);
if (inEdges) {
return inEdges.concat(this.outEdges(v, w));
}
}
}
function incrementOrInitEntry(map, k) {
if (map[k]) {
map[k]++;
} else {
map[k] = 1;
}
}
function decrementOrRemoveEntry(map, k) {
if (!--map[k]) { delete map[k]; }
}
function edgeArgsToId(isDirected, v_, w_, name) {
var v = "" + v_;
var w = "" + w_;
if (!isDirected && v > w) {
var tmp = v;
v = w;
w = tmp;
}
return v + EDGE_KEY_DELIM + w + EDGE_KEY_DELIM +
(name === undefined ? DEFAULT_EDGE_NAME : name);
}
function edgeArgsToObj(isDirected, v_, w_, name) {
var v = "" + v_;
var w = "" + w_;
if (!isDirected && v > w) {
var tmp = v;
v = w;
w = tmp;
}
var edgeObj = { v: v, w: w };
if (name) {
edgeObj.name = name;
}
return edgeObj;
}
function edgeObjToId(isDirected, edgeObj) {
return edgeArgsToId(isDirected, edgeObj.v, edgeObj.w, edgeObj.name);
}
module.exports = Graph;