UNPKG

@dagrejs/graphlib

Version:

A directed and undirected multi-graph library

697 lines (608 loc) 17.2 kB
"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;