You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
988 lines
32 KiB
988 lines
32 KiB
|
4 months ago
|
import EventEmitter from '@antv/event-emitter';
|
||
|
|
import { GraphView } from './graphView';
|
||
|
|
import { doBFS, doDFS } from './utils/traverse';
|
||
|
|
export class Graph extends EventEmitter {
|
||
|
|
nodeMap = new Map();
|
||
|
|
edgeMap = new Map();
|
||
|
|
inEdgesMap = new Map();
|
||
|
|
outEdgesMap = new Map();
|
||
|
|
bothEdgesMap = new Map();
|
||
|
|
treeIndices = new Map();
|
||
|
|
changes = [];
|
||
|
|
batchCount = 0;
|
||
|
|
/**
|
||
|
|
* This function is called with a {@link GraphChangedEvent} each time a graph change happened.
|
||
|
|
*
|
||
|
|
* `event.changes` contains all the graph changes in order since last `onChanged`.
|
||
|
|
*/
|
||
|
|
onChanged = () => {
|
||
|
|
// Do nothing.
|
||
|
|
};
|
||
|
|
/**
|
||
|
|
* Create a new Graph instance.
|
||
|
|
* @param options - The options to initialize a graph. See {@link GraphOptions}.
|
||
|
|
*
|
||
|
|
* ```ts
|
||
|
|
* const graph = new Graph({
|
||
|
|
* // Optional, initial nodes.
|
||
|
|
* nodes: [
|
||
|
|
* // Each node has a unique ID.
|
||
|
|
* { id: 'A', foo: 1 },
|
||
|
|
* { id: 'B', foo: 1 },
|
||
|
|
* ],
|
||
|
|
* // Optional, initial edges.
|
||
|
|
* edges: [
|
||
|
|
* { id: 'C', source: 'B', target: 'B', weight: 1 },
|
||
|
|
* ],
|
||
|
|
* // Optional, called with a GraphChangedEvent.
|
||
|
|
* onChanged: (event) => {
|
||
|
|
* console.log(event);
|
||
|
|
* }
|
||
|
|
* });
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
constructor(options) {
|
||
|
|
super();
|
||
|
|
if (!options)
|
||
|
|
return;
|
||
|
|
if (options.nodes)
|
||
|
|
this.addNodes(options.nodes);
|
||
|
|
if (options.edges)
|
||
|
|
this.addEdges(options.edges);
|
||
|
|
if (options.tree)
|
||
|
|
this.addTree(options.tree);
|
||
|
|
if (options.onChanged)
|
||
|
|
this.onChanged = options.onChanged;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Batch several graph changes into one.
|
||
|
|
*
|
||
|
|
* Make several changes, but dispatch only one ChangedEvent at the end of batch:
|
||
|
|
* ```ts
|
||
|
|
* graph.batch(() => {
|
||
|
|
* graph.addNodes([]);
|
||
|
|
* graph.addEdges([]);
|
||
|
|
* });
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* Batches can be nested. Only the outermost batch will dispatch a ChangedEvent:
|
||
|
|
* ```ts
|
||
|
|
* graph.batch(() => {
|
||
|
|
* graph.addNodes([]);
|
||
|
|
* graph.batch(() => {
|
||
|
|
* graph.addEdges([]);
|
||
|
|
* });
|
||
|
|
* });
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
batch = (fn) => {
|
||
|
|
this.batchCount += 1;
|
||
|
|
fn();
|
||
|
|
this.batchCount -= 1;
|
||
|
|
if (!this.batchCount) {
|
||
|
|
this.commit();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
/**
|
||
|
|
* Reset changes and dispatch a ChangedEvent.
|
||
|
|
*/
|
||
|
|
commit() {
|
||
|
|
const changes = this.changes;
|
||
|
|
this.changes = [];
|
||
|
|
const event = {
|
||
|
|
graph: this,
|
||
|
|
changes,
|
||
|
|
};
|
||
|
|
this.emit('changed', event);
|
||
|
|
this.onChanged(event);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Reduce the number of ordered graph changes by dropping or merging unnecessary changes.
|
||
|
|
*
|
||
|
|
* For example, if we update a node and remove it in a batch:
|
||
|
|
*
|
||
|
|
* ```ts
|
||
|
|
* graph.batch(() => {
|
||
|
|
* graph.updateNodeData('A', 'foo', 2);
|
||
|
|
* graph.removeNode('A');
|
||
|
|
* });
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* We get 2 atomic graph changes like
|
||
|
|
*
|
||
|
|
* ```ts
|
||
|
|
* [
|
||
|
|
* { type: 'NodeDataUpdated', id: 'A', propertyName: 'foo', oldValue: 1, newValue: 2 },
|
||
|
|
* { type: 'NodeRemoved', value: { id: 'A', data: { foo: 2 } },
|
||
|
|
* ]
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* Since node 'A' has been removed, we actually have no need to handle with NodeDataUpdated change.
|
||
|
|
*
|
||
|
|
* `reduceChanges()` here helps us remove such changes.
|
||
|
|
*/
|
||
|
|
reduceChanges(changes) {
|
||
|
|
let mergedChanges = [];
|
||
|
|
changes.forEach((change) => {
|
||
|
|
switch (change.type) {
|
||
|
|
case 'NodeRemoved': {
|
||
|
|
// NodeAdded: A added.
|
||
|
|
// NodeDataUpdated: A changed.
|
||
|
|
// TreeStructureChanged: A's parent changed.
|
||
|
|
// NodeRemoved: A removed. 👈🏻 Since A was removed, above three changes may be ignored.
|
||
|
|
let isNewlyAdded = false;
|
||
|
|
mergedChanges = mergedChanges.filter((pastChange) => {
|
||
|
|
if (pastChange.type === 'NodeAdded') {
|
||
|
|
const sameId = pastChange.value.id === change.value.id;
|
||
|
|
if (sameId) {
|
||
|
|
isNewlyAdded = true;
|
||
|
|
}
|
||
|
|
return !sameId;
|
||
|
|
}
|
||
|
|
else if (pastChange.type === 'NodeDataUpdated') {
|
||
|
|
return pastChange.id !== change.value.id;
|
||
|
|
}
|
||
|
|
else if (pastChange.type === 'TreeStructureChanged') {
|
||
|
|
return pastChange.nodeId !== change.value.id;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
});
|
||
|
|
if (!isNewlyAdded) {
|
||
|
|
mergedChanges.push(change);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case 'EdgeRemoved': {
|
||
|
|
// EdgeAdded: A added.
|
||
|
|
// EdgeDataUpdated: A changed.
|
||
|
|
// EdgeDataUpdated: A's source/target changed.
|
||
|
|
// EdgeRemoved: A removed. 👈🏻 Since A was removed, above three changes may be ignored.
|
||
|
|
let isNewlyAdded = false;
|
||
|
|
mergedChanges = mergedChanges.filter((pastChange) => {
|
||
|
|
if (pastChange.type === 'EdgeAdded') {
|
||
|
|
const sameId = pastChange.value.id === change.value.id;
|
||
|
|
if (sameId) {
|
||
|
|
isNewlyAdded = true;
|
||
|
|
}
|
||
|
|
return !sameId;
|
||
|
|
}
|
||
|
|
else if (pastChange.type === 'EdgeDataUpdated' ||
|
||
|
|
pastChange.type === 'EdgeUpdated') {
|
||
|
|
return pastChange.id !== change.value.id;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
});
|
||
|
|
if (!isNewlyAdded) {
|
||
|
|
mergedChanges.push(change);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case 'NodeDataUpdated':
|
||
|
|
case 'EdgeDataUpdated':
|
||
|
|
case 'EdgeUpdated': {
|
||
|
|
// NodeDataUpdated: { id: A, propertyName: 'foo', oldValue: 1, newValue: 2 }.
|
||
|
|
// NodeDataUpdated: { id: A, propertyName: 'foo', oldValue: 2, newValue: 3 }.
|
||
|
|
// 👆 Could be merged as { id: A, propertyName: 'foo', oldValue: 1, newValue: 3 }.
|
||
|
|
const index = mergedChanges.findIndex((pastChange) => {
|
||
|
|
return (pastChange.type === change.type &&
|
||
|
|
pastChange.id === change.id &&
|
||
|
|
(change.propertyName === undefined ||
|
||
|
|
pastChange.propertyName === change.propertyName));
|
||
|
|
});
|
||
|
|
const existingChange = mergedChanges[index];
|
||
|
|
if (existingChange) {
|
||
|
|
if (change.propertyName !== undefined) {
|
||
|
|
// The incoming change is of the same property of existing change.
|
||
|
|
existingChange.newValue = change.newValue;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
// The incoming change is a whole data override.
|
||
|
|
mergedChanges.splice(index, 1);
|
||
|
|
mergedChanges.push(change);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
mergedChanges.push(change);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case 'TreeStructureDetached': {
|
||
|
|
// TreeStructureAttached
|
||
|
|
// TreeStructureChanged
|
||
|
|
// TreeStructureDetached 👈🏻 Since the tree struct was detached, above 2 changes may be ignored.
|
||
|
|
mergedChanges = mergedChanges.filter((pastChange) => {
|
||
|
|
if (pastChange.type === 'TreeStructureAttached') {
|
||
|
|
return pastChange.treeKey !== change.treeKey;
|
||
|
|
}
|
||
|
|
else if (pastChange.type === 'TreeStructureChanged') {
|
||
|
|
return pastChange.treeKey !== change.treeKey;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
});
|
||
|
|
mergedChanges.push(change);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case 'TreeStructureChanged': {
|
||
|
|
const existingChange = mergedChanges.find((pastChange) => {
|
||
|
|
return (pastChange.type === 'TreeStructureChanged' &&
|
||
|
|
pastChange.treeKey === change.treeKey &&
|
||
|
|
pastChange.nodeId === change.nodeId);
|
||
|
|
});
|
||
|
|
if (existingChange) {
|
||
|
|
existingChange.newParentId =
|
||
|
|
change.newParentId;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
mergedChanges.push(change);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
mergedChanges.push(change);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return mergedChanges;
|
||
|
|
}
|
||
|
|
// ================= Node =================
|
||
|
|
checkNodeExistence(id) {
|
||
|
|
this.getNode(id);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if a node exists in the graph.
|
||
|
|
* @group NodeMethods
|
||
|
|
*/
|
||
|
|
hasNode(id) {
|
||
|
|
return this.nodeMap.has(id);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Tell if two nodes are neighbors.
|
||
|
|
* @group NodeMethods
|
||
|
|
*/
|
||
|
|
areNeighbors(firstNodeId, secondNodeId) {
|
||
|
|
return this.getNeighbors(secondNodeId).some((neighbor) => neighbor.id === firstNodeId);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the node data with given ID.
|
||
|
|
* @group NodeMethods
|
||
|
|
*/
|
||
|
|
getNode(id) {
|
||
|
|
const node = this.nodeMap.get(id);
|
||
|
|
if (!node) {
|
||
|
|
throw new Error('Node not found for id: ' + id);
|
||
|
|
}
|
||
|
|
return node;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Given a node ID, find all edges of the node.
|
||
|
|
* @param id - ID of the node
|
||
|
|
* @param direction - Edge direction, defaults to 'both'.
|
||
|
|
* @group NodeMethods
|
||
|
|
*/
|
||
|
|
getRelatedEdges(id, direction) {
|
||
|
|
this.checkNodeExistence(id);
|
||
|
|
if (direction === 'in') {
|
||
|
|
const inEdges = this.inEdgesMap.get(id);
|
||
|
|
return Array.from(inEdges);
|
||
|
|
}
|
||
|
|
else if (direction === 'out') {
|
||
|
|
const outEdges = this.outEdgesMap.get(id);
|
||
|
|
return Array.from(outEdges);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
const bothEdges = this.bothEdgesMap.get(id);
|
||
|
|
return Array.from(bothEdges);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the degree of the given node.
|
||
|
|
* @group NodeMethods
|
||
|
|
*/
|
||
|
|
getDegree(id, direction) {
|
||
|
|
return this.getRelatedEdges(id, direction).length;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get all successors of the given node.
|
||
|
|
*/
|
||
|
|
getSuccessors(id) {
|
||
|
|
const outEdges = this.getRelatedEdges(id, 'out');
|
||
|
|
const targets = outEdges.map((edge) => this.getNode(edge.target));
|
||
|
|
return Array.from(new Set(targets));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get all predecessors of the given node.
|
||
|
|
*/
|
||
|
|
getPredecessors(id) {
|
||
|
|
const inEdges = this.getRelatedEdges(id, 'in');
|
||
|
|
const sources = inEdges.map((edge) => this.getNode(edge.source));
|
||
|
|
return Array.from(new Set(sources));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Given a node ID, find its neighbors.
|
||
|
|
* @param id - ID of the node
|
||
|
|
* @group NodeMethods
|
||
|
|
*/
|
||
|
|
getNeighbors(id) {
|
||
|
|
const predecessors = this.getPredecessors(id);
|
||
|
|
const successors = this.getSuccessors(id);
|
||
|
|
return Array.from(new Set([...predecessors, ...successors]));
|
||
|
|
}
|
||
|
|
doAddNode(node) {
|
||
|
|
if (this.hasNode(node.id)) {
|
||
|
|
throw new Error('Node already exists: ' + node.id);
|
||
|
|
}
|
||
|
|
this.nodeMap.set(node.id, node);
|
||
|
|
this.inEdgesMap.set(node.id, new Set());
|
||
|
|
this.outEdgesMap.set(node.id, new Set());
|
||
|
|
this.bothEdgesMap.set(node.id, new Set());
|
||
|
|
this.treeIndices.forEach((tree) => {
|
||
|
|
tree.childrenMap.set(node.id, new Set());
|
||
|
|
});
|
||
|
|
this.changes.push({ type: 'NodeAdded', value: node });
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Add all nodes of the given array, or iterable, into the graph.
|
||
|
|
* @group NodeMethods
|
||
|
|
*/
|
||
|
|
addNodes(nodes) {
|
||
|
|
this.batch(() => {
|
||
|
|
for (const node of nodes) {
|
||
|
|
this.doAddNode(node);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Add a single node into the graph.
|
||
|
|
* @group NodeMethods
|
||
|
|
*/
|
||
|
|
addNode(node) {
|
||
|
|
this.addNodes([node]);
|
||
|
|
}
|
||
|
|
doRemoveNode(id) {
|
||
|
|
const node = this.getNode(id);
|
||
|
|
const bothEdges = this.bothEdgesMap.get(id);
|
||
|
|
bothEdges?.forEach((edge) => this.doRemoveEdge(edge.id));
|
||
|
|
this.nodeMap.delete(id);
|
||
|
|
this.treeIndices.forEach((tree) => {
|
||
|
|
tree.childrenMap.get(id)?.forEach((child) => {
|
||
|
|
tree.parentMap.delete(child.id);
|
||
|
|
});
|
||
|
|
const parent = tree.parentMap.get(id);
|
||
|
|
if (parent)
|
||
|
|
tree.childrenMap.get(parent.id)?.delete(node);
|
||
|
|
tree.parentMap.delete(id);
|
||
|
|
tree.childrenMap.delete(id);
|
||
|
|
});
|
||
|
|
this.bothEdgesMap.delete(id);
|
||
|
|
this.inEdgesMap.delete(id);
|
||
|
|
this.outEdgesMap.delete(id);
|
||
|
|
this.changes.push({ type: 'NodeRemoved', value: node });
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Remove nodes and their attached edges from the graph.
|
||
|
|
* @group NodeMethods
|
||
|
|
*/
|
||
|
|
removeNodes(idList) {
|
||
|
|
this.batch(() => {
|
||
|
|
idList.forEach((id) => this.doRemoveNode(id));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Remove a single node and its attached edges from the graph.
|
||
|
|
* @group NodeMethods
|
||
|
|
*/
|
||
|
|
removeNode(id) {
|
||
|
|
this.removeNodes([id]);
|
||
|
|
}
|
||
|
|
updateNodeDataProperty(id, propertyName, value) {
|
||
|
|
const node = this.getNode(id);
|
||
|
|
this.batch(() => {
|
||
|
|
const oldValue = node.data[propertyName];
|
||
|
|
const newValue = value;
|
||
|
|
node.data[propertyName] = newValue;
|
||
|
|
this.changes.push({
|
||
|
|
type: 'NodeDataUpdated',
|
||
|
|
id,
|
||
|
|
propertyName,
|
||
|
|
oldValue,
|
||
|
|
newValue,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Like Object.assign, merge all properties of `path` to the node data.
|
||
|
|
* @param id Node ID.
|
||
|
|
* @param patch A data object to merge.
|
||
|
|
*/
|
||
|
|
mergeNodeData(id, patch) {
|
||
|
|
this.batch(() => {
|
||
|
|
Object.entries(patch).forEach(([propertyName, value]) => {
|
||
|
|
this.updateNodeDataProperty(id, propertyName, value);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
updateNodeData(...args) {
|
||
|
|
const id = args[0];
|
||
|
|
const node = this.getNode(id);
|
||
|
|
if (typeof args[1] === 'string') {
|
||
|
|
// id, propertyName, value
|
||
|
|
this.updateNodeDataProperty(id, args[1], args[2]);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let data;
|
||
|
|
if (typeof args[1] === 'function') {
|
||
|
|
// id, update
|
||
|
|
const update = args[1];
|
||
|
|
data = update(node.data);
|
||
|
|
}
|
||
|
|
else if (typeof args[1] === 'object') {
|
||
|
|
// id, data
|
||
|
|
data = args[1];
|
||
|
|
}
|
||
|
|
this.batch(() => {
|
||
|
|
const oldValue = node.data;
|
||
|
|
const newValue = data;
|
||
|
|
node.data = data;
|
||
|
|
this.changes.push({
|
||
|
|
type: 'NodeDataUpdated',
|
||
|
|
id,
|
||
|
|
oldValue,
|
||
|
|
newValue,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// ================= Edge =================
|
||
|
|
checkEdgeExistence(id) {
|
||
|
|
if (!this.hasEdge(id)) {
|
||
|
|
throw new Error('Edge not found for id: ' + id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if an edge exists in the graph.
|
||
|
|
* @group NodeMethods
|
||
|
|
*/
|
||
|
|
hasEdge(id) {
|
||
|
|
return this.edgeMap.has(id);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the edge data with given ID.
|
||
|
|
* @group EdgeMethods
|
||
|
|
*/
|
||
|
|
getEdge(id) {
|
||
|
|
this.checkEdgeExistence(id);
|
||
|
|
return this.edgeMap.get(id);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the edge, the source node, and the target node by an edge ID.
|
||
|
|
* @group EdgeMethods
|
||
|
|
*/
|
||
|
|
getEdgeDetail(id) {
|
||
|
|
const edge = this.getEdge(id);
|
||
|
|
return {
|
||
|
|
edge,
|
||
|
|
source: this.getNode(edge.source),
|
||
|
|
target: this.getNode(edge.target),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
doAddEdge(edge) {
|
||
|
|
if (this.hasEdge(edge.id)) {
|
||
|
|
throw new Error('Edge already exists: ' + edge.id);
|
||
|
|
}
|
||
|
|
this.checkNodeExistence(edge.source);
|
||
|
|
this.checkNodeExistence(edge.target);
|
||
|
|
this.edgeMap.set(edge.id, edge);
|
||
|
|
const inEdges = this.inEdgesMap.get(edge.target);
|
||
|
|
const outEdges = this.outEdgesMap.get(edge.source);
|
||
|
|
const bothEdgesOfSource = this.bothEdgesMap.get(edge.source);
|
||
|
|
const bothEdgesOfTarget = this.bothEdgesMap.get(edge.target);
|
||
|
|
inEdges.add(edge);
|
||
|
|
outEdges.add(edge);
|
||
|
|
bothEdgesOfSource.add(edge);
|
||
|
|
bothEdgesOfTarget.add(edge);
|
||
|
|
this.changes.push({ type: 'EdgeAdded', value: edge });
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Add all edges of the given iterable(an array, a set, etc.) into the graph.
|
||
|
|
* @group EdgeMethods
|
||
|
|
*/
|
||
|
|
addEdges(edges) {
|
||
|
|
this.batch(() => {
|
||
|
|
for (const edge of edges) {
|
||
|
|
this.doAddEdge(edge);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Add a single edge pointing from `source` to `target` into the graph.
|
||
|
|
*
|
||
|
|
* ```ts
|
||
|
|
* graph.addNode({ id: 'NodeA' });
|
||
|
|
* graph.addNode({ id: 'NodeB' });
|
||
|
|
* graph.addEdge({ id: 'EdgeA', source: 'NodeA', target: 'NodeB' });
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* If `source` or `target` were not found in the current graph, it throws an Error.
|
||
|
|
* @group EdgeMethods
|
||
|
|
*/
|
||
|
|
addEdge(edge) {
|
||
|
|
this.addEdges([edge]);
|
||
|
|
}
|
||
|
|
doRemoveEdge(id) {
|
||
|
|
const edge = this.getEdge(id);
|
||
|
|
const outEdges = this.outEdgesMap.get(edge.source);
|
||
|
|
const inEdges = this.inEdgesMap.get(edge.target);
|
||
|
|
const bothEdgesOfSource = this.bothEdgesMap.get(edge.source);
|
||
|
|
const bothEdgesOfTarget = this.bothEdgesMap.get(edge.target);
|
||
|
|
outEdges.delete(edge);
|
||
|
|
inEdges.delete(edge);
|
||
|
|
bothEdgesOfSource.delete(edge);
|
||
|
|
bothEdgesOfTarget.delete(edge);
|
||
|
|
this.edgeMap.delete(id);
|
||
|
|
this.changes.push({ type: 'EdgeRemoved', value: edge });
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Remove edges whose id was included in the given id list.
|
||
|
|
* @group EdgeMethods
|
||
|
|
*/
|
||
|
|
removeEdges(idList) {
|
||
|
|
this.batch(() => {
|
||
|
|
idList.forEach((id) => this.doRemoveEdge(id));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Remove a single edge of the given id.
|
||
|
|
* @group EdgeMethods
|
||
|
|
*/
|
||
|
|
removeEdge(id) {
|
||
|
|
this.removeEdges([id]);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Change the source of an edge. The source must be found in current graph.
|
||
|
|
* @group EdgeMethods
|
||
|
|
*/
|
||
|
|
updateEdgeSource(id, source) {
|
||
|
|
const edge = this.getEdge(id);
|
||
|
|
this.checkNodeExistence(source);
|
||
|
|
const oldSource = edge.source;
|
||
|
|
const newSource = source;
|
||
|
|
this.outEdgesMap.get(oldSource).delete(edge);
|
||
|
|
this.bothEdgesMap.get(oldSource).delete(edge);
|
||
|
|
this.outEdgesMap.get(newSource).add(edge);
|
||
|
|
this.bothEdgesMap.get(newSource).add(edge);
|
||
|
|
edge.source = source;
|
||
|
|
this.batch(() => {
|
||
|
|
this.changes.push({
|
||
|
|
type: 'EdgeUpdated',
|
||
|
|
id,
|
||
|
|
propertyName: 'source',
|
||
|
|
oldValue: oldSource,
|
||
|
|
newValue: newSource,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Change the target of an edge. The target must be found in current graph.
|
||
|
|
* @group EdgeMethods
|
||
|
|
*/
|
||
|
|
updateEdgeTarget(id, target) {
|
||
|
|
const edge = this.getEdge(id);
|
||
|
|
this.checkNodeExistence(target);
|
||
|
|
const oldTarget = edge.target;
|
||
|
|
const newTarget = target;
|
||
|
|
this.inEdgesMap.get(oldTarget).delete(edge);
|
||
|
|
this.bothEdgesMap.get(oldTarget).delete(edge);
|
||
|
|
this.inEdgesMap.get(newTarget).add(edge);
|
||
|
|
this.bothEdgesMap.get(newTarget).add(edge);
|
||
|
|
edge.target = target;
|
||
|
|
this.batch(() => {
|
||
|
|
this.changes.push({
|
||
|
|
type: 'EdgeUpdated',
|
||
|
|
id,
|
||
|
|
propertyName: 'target',
|
||
|
|
oldValue: oldTarget,
|
||
|
|
newValue: newTarget,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
updateEdgeDataProperty(id, propertyName, value) {
|
||
|
|
const edge = this.getEdge(id);
|
||
|
|
this.batch(() => {
|
||
|
|
const oldValue = edge.data[propertyName];
|
||
|
|
const newValue = value;
|
||
|
|
edge.data[propertyName] = newValue;
|
||
|
|
this.changes.push({
|
||
|
|
type: 'EdgeDataUpdated',
|
||
|
|
id,
|
||
|
|
propertyName,
|
||
|
|
oldValue,
|
||
|
|
newValue,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
updateEdgeData(...args) {
|
||
|
|
const id = args[0];
|
||
|
|
const edge = this.getEdge(id);
|
||
|
|
if (typeof args[1] === 'string') {
|
||
|
|
// id, propertyName, value
|
||
|
|
this.updateEdgeDataProperty(id, args[1], args[2]);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let data;
|
||
|
|
if (typeof args[1] === 'function') {
|
||
|
|
// id, update
|
||
|
|
const update = args[1];
|
||
|
|
data = update(edge.data);
|
||
|
|
}
|
||
|
|
else if (typeof args[1] === 'object') {
|
||
|
|
// id, data
|
||
|
|
data = args[1];
|
||
|
|
}
|
||
|
|
this.batch(() => {
|
||
|
|
const oldValue = edge.data;
|
||
|
|
const newValue = data;
|
||
|
|
edge.data = data;
|
||
|
|
this.changes.push({
|
||
|
|
type: 'EdgeDataUpdated',
|
||
|
|
id,
|
||
|
|
oldValue,
|
||
|
|
newValue,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* @group EdgeMethods
|
||
|
|
*/
|
||
|
|
mergeEdgeData(id, patch) {
|
||
|
|
this.batch(() => {
|
||
|
|
Object.entries(patch).forEach(([propertyName, value]) => {
|
||
|
|
this.updateEdgeDataProperty(id, propertyName, value);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// ================= Tree =================
|
||
|
|
checkTreeExistence(treeKey) {
|
||
|
|
if (!this.hasTreeStructure(treeKey)) {
|
||
|
|
throw new Error('Tree structure not found for treeKey: ' + treeKey);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
hasTreeStructure(treeKey) {
|
||
|
|
return this.treeIndices.has(treeKey);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Attach a new tree structure representing the hierarchy of all nodes in the graph.
|
||
|
|
* @param treeKey A unique key of the tree structure. You can attach multiple tree structures with different keys.
|
||
|
|
*
|
||
|
|
* ```ts
|
||
|
|
* const graph = new Graph({
|
||
|
|
* nodes: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||
|
|
* });
|
||
|
|
* graph.attachTreeStructure('Inheritance');
|
||
|
|
* graph.setParent(2, 1, 'Inheritance');
|
||
|
|
* graph.setParent(3, 1, 'Inheritance');
|
||
|
|
* graph.getRoots('Inheritance'); // [1]
|
||
|
|
* graph.getChildren(1, 'Inheritance'); // [2,3]
|
||
|
|
* ```
|
||
|
|
* @group TreeMethods
|
||
|
|
*/
|
||
|
|
attachTreeStructure(treeKey) {
|
||
|
|
if (this.treeIndices.has(treeKey)) {
|
||
|
|
// Already attached.
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.treeIndices.set(treeKey, {
|
||
|
|
parentMap: new Map(),
|
||
|
|
childrenMap: new Map(),
|
||
|
|
});
|
||
|
|
this.batch(() => {
|
||
|
|
this.changes.push({
|
||
|
|
type: 'TreeStructureAttached',
|
||
|
|
treeKey,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Detach the tree structure of the given tree key from the graph.
|
||
|
|
*
|
||
|
|
* ```ts
|
||
|
|
* graph.detachTreeStructure('Inheritance');
|
||
|
|
* graph.getRoots('Inheritance'); // Error!
|
||
|
|
* ```
|
||
|
|
* @group TreeMethods
|
||
|
|
*/
|
||
|
|
detachTreeStructure(treeKey) {
|
||
|
|
this.checkTreeExistence(treeKey);
|
||
|
|
this.treeIndices.delete(treeKey);
|
||
|
|
this.batch(() => {
|
||
|
|
this.changes.push({
|
||
|
|
type: 'TreeStructureDetached',
|
||
|
|
treeKey,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Traverse the given tree data, add each node into the graph, then attach the tree structure.
|
||
|
|
*
|
||
|
|
* ```ts
|
||
|
|
* graph.addTree({
|
||
|
|
* id: 1,
|
||
|
|
* children: [
|
||
|
|
* { id: 2 },
|
||
|
|
* { id: 3 },
|
||
|
|
* ],
|
||
|
|
* }, 'Inheritance');
|
||
|
|
* graph.getRoots('Inheritance'); // [1]
|
||
|
|
* graph.getChildren(1, 'Inheritance'); // [2, 3]
|
||
|
|
* graph.getAllNodes(); // [1, 2, 3]
|
||
|
|
* graph.getAllEdges(); // []
|
||
|
|
* ```
|
||
|
|
* @group TreeMethods
|
||
|
|
*/
|
||
|
|
addTree(tree, treeKey) {
|
||
|
|
this.batch(() => {
|
||
|
|
this.attachTreeStructure(treeKey);
|
||
|
|
// Add Nodes
|
||
|
|
const nodes = [];
|
||
|
|
const stack = Array.isArray(tree) ? tree : [tree];
|
||
|
|
while (stack.length) {
|
||
|
|
const node = stack.shift();
|
||
|
|
nodes.push(node);
|
||
|
|
if (node.children) {
|
||
|
|
stack.push(...node.children);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this.addNodes(nodes);
|
||
|
|
// Set parent for each child node.
|
||
|
|
nodes.forEach((parent) => {
|
||
|
|
parent.children?.forEach((child) => {
|
||
|
|
this.setParent(child.id, parent.id, treeKey);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the root nodes of an attached tree structure.
|
||
|
|
*
|
||
|
|
* Consider a graph with the following tree structure attached:
|
||
|
|
* ```
|
||
|
|
* Tree structure:
|
||
|
|
* O 3
|
||
|
|
* / \ |
|
||
|
|
* 1 2 4
|
||
|
|
* ```
|
||
|
|
* `graph.getRoots()` takes all nodes without a parent, therefore [0, 3] was returned.
|
||
|
|
*
|
||
|
|
* Newly added nodes are also unparented. So they are counted as roots.
|
||
|
|
* ```ts
|
||
|
|
* graph.addNode({ id: 5 });
|
||
|
|
* graph.getRoots(); // [0, 3, 5]
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* Here is how the tree structure looks like:
|
||
|
|
* ```
|
||
|
|
* Tree structure:
|
||
|
|
* O 3 5
|
||
|
|
* / \ |
|
||
|
|
* 1 2 4
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* By setting a parent, a root node no more be a root.
|
||
|
|
* ```ts
|
||
|
|
* graph.setParent(5, 2);
|
||
|
|
* graph.getRoots(); // [0, 3]
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* The tree structure now becomes:
|
||
|
|
* ```
|
||
|
|
* Tree structure:
|
||
|
|
* O 3
|
||
|
|
* / \ |
|
||
|
|
* 1 2 4
|
||
|
|
* |
|
||
|
|
* 5
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* Removing a node forces its children to be unparented, or roots.
|
||
|
|
* ```ts
|
||
|
|
* graph.removeNode(0);
|
||
|
|
* graph.getRoots(); // [1, 2, 3]
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* You might draw the the structure as follow:
|
||
|
|
* ```
|
||
|
|
* Tree structure:
|
||
|
|
* 1 2 3
|
||
|
|
* | |
|
||
|
|
* 5 4
|
||
|
|
* ```
|
||
|
|
* @group TreeMethods
|
||
|
|
*/
|
||
|
|
getRoots(treeKey) {
|
||
|
|
this.checkTreeExistence(treeKey);
|
||
|
|
return this.getAllNodes().filter((node) => !this.getParent(node.id, treeKey));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Given a node ID and an optional tree key, get the children of the node in the specified tree structure.
|
||
|
|
* @group TreeMethods
|
||
|
|
*/
|
||
|
|
getChildren(id, treeKey) {
|
||
|
|
this.checkNodeExistence(id);
|
||
|
|
this.checkTreeExistence(treeKey);
|
||
|
|
const tree = this.treeIndices.get(treeKey);
|
||
|
|
const children = tree.childrenMap.get(id);
|
||
|
|
return Array.from(children || []);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Given a node ID and an optional tree key, get the parent of the node in the specified tree structure.
|
||
|
|
* If the given node is one of the tree roots, this returns null.
|
||
|
|
* @group TreeMethods
|
||
|
|
*/
|
||
|
|
getParent(id, treeKey) {
|
||
|
|
this.checkNodeExistence(id);
|
||
|
|
this.checkTreeExistence(treeKey);
|
||
|
|
const tree = this.treeIndices.get(treeKey);
|
||
|
|
return tree.parentMap.get(id) || null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns an array of all the ancestor nodes, staring from the parent to the root.
|
||
|
|
*/
|
||
|
|
getAncestors(id, treeKey) {
|
||
|
|
const ancestors = [];
|
||
|
|
let current = this.getNode(id);
|
||
|
|
let parent;
|
||
|
|
// eslint-disable-next-line no-cond-assign
|
||
|
|
while ((parent = this.getParent(current.id, treeKey))) {
|
||
|
|
ancestors.push(parent);
|
||
|
|
current = parent;
|
||
|
|
}
|
||
|
|
return ancestors;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set node parent. If this operation causes a circle, it fails with an error.
|
||
|
|
* @param id - ID of the child node.
|
||
|
|
* @param parent - ID of the parent node. If it is undefined or null, means unset parent for node with id.
|
||
|
|
* @param treeKey - Which tree structure the relation is applied to.
|
||
|
|
* @group TreeMethods
|
||
|
|
*/
|
||
|
|
setParent(id, parent, treeKey) {
|
||
|
|
this.checkTreeExistence(treeKey);
|
||
|
|
const tree = this.treeIndices.get(treeKey);
|
||
|
|
if (!tree)
|
||
|
|
return;
|
||
|
|
const node = this.getNode(id);
|
||
|
|
const oldParent = tree.parentMap.get(id);
|
||
|
|
// Same parent id as old one, skip
|
||
|
|
if (oldParent?.id === parent)
|
||
|
|
return;
|
||
|
|
// New parent is undefined or null, unset parent for the node
|
||
|
|
if (parent === undefined || parent === null) {
|
||
|
|
if (oldParent) {
|
||
|
|
tree.childrenMap.get(oldParent.id)?.delete(node);
|
||
|
|
}
|
||
|
|
tree.parentMap.delete(id);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const newParent = this.getNode(parent);
|
||
|
|
// Set parent
|
||
|
|
tree.parentMap.set(id, newParent);
|
||
|
|
// Set children
|
||
|
|
if (oldParent) {
|
||
|
|
tree.childrenMap.get(oldParent.id)?.delete(node);
|
||
|
|
}
|
||
|
|
let children = tree.childrenMap.get(newParent.id);
|
||
|
|
if (!children) {
|
||
|
|
children = new Set();
|
||
|
|
tree.childrenMap.set(newParent.id, children);
|
||
|
|
}
|
||
|
|
children.add(node);
|
||
|
|
this.batch(() => {
|
||
|
|
this.changes.push({
|
||
|
|
type: 'TreeStructureChanged',
|
||
|
|
treeKey,
|
||
|
|
nodeId: id,
|
||
|
|
oldParentId: oldParent?.id,
|
||
|
|
newParentId: newParent.id,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
dfsTree(id, fn, treeKey) {
|
||
|
|
const navigator = (nodeId) => this.getChildren(nodeId, treeKey);
|
||
|
|
return doDFS(this.getNode(id), new Set(), fn, navigator);
|
||
|
|
}
|
||
|
|
bfsTree(id, fn, treeKey) {
|
||
|
|
const navigator = (nodeId) => this.getChildren(nodeId, treeKey);
|
||
|
|
return doBFS([this.getNode(id)], new Set(), fn, navigator);
|
||
|
|
}
|
||
|
|
// ================= Graph =================
|
||
|
|
/**
|
||
|
|
* Get all nodes in the graph as an array.
|
||
|
|
*/
|
||
|
|
getAllNodes() {
|
||
|
|
return Array.from(this.nodeMap.values());
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get all edges in the graph as an array.
|
||
|
|
*/
|
||
|
|
getAllEdges() {
|
||
|
|
return Array.from(this.edgeMap.values());
|
||
|
|
}
|
||
|
|
bfs(id, fn, direction = 'out') {
|
||
|
|
const navigator = {
|
||
|
|
in: this.getPredecessors.bind(this),
|
||
|
|
out: this.getSuccessors.bind(this),
|
||
|
|
both: this.getNeighbors.bind(this),
|
||
|
|
}[direction];
|
||
|
|
return doBFS([this.getNode(id)], new Set(), fn, navigator);
|
||
|
|
}
|
||
|
|
dfs(id, fn, direction = 'out') {
|
||
|
|
const navigator = {
|
||
|
|
in: this.getPredecessors.bind(this),
|
||
|
|
out: this.getSuccessors.bind(this),
|
||
|
|
both: this.getNeighbors.bind(this),
|
||
|
|
}[direction];
|
||
|
|
return doDFS(this.getNode(id), new Set(), fn, navigator);
|
||
|
|
}
|
||
|
|
clone() {
|
||
|
|
// Make a shallow copy of nodes and edges.
|
||
|
|
const newNodes = this.getAllNodes().map((oldNode) => {
|
||
|
|
return { ...oldNode, data: { ...oldNode.data } };
|
||
|
|
});
|
||
|
|
const newEdges = this.getAllEdges().map((oldEdge) => {
|
||
|
|
return { ...oldEdge, data: { ...oldEdge.data } };
|
||
|
|
});
|
||
|
|
// Create a new graph with shallow copied nodes and edges.
|
||
|
|
const newGraph = new Graph({
|
||
|
|
nodes: newNodes,
|
||
|
|
edges: newEdges,
|
||
|
|
});
|
||
|
|
// Add tree indices.
|
||
|
|
this.treeIndices.forEach(({ parentMap: oldParentMap, childrenMap: oldChildrenMap }, treeKey) => {
|
||
|
|
const parentMap = new Map();
|
||
|
|
oldParentMap.forEach((parent, key) => {
|
||
|
|
parentMap.set(key, newGraph.getNode(parent.id));
|
||
|
|
});
|
||
|
|
const childrenMap = new Map();
|
||
|
|
oldChildrenMap.forEach((children, key) => {
|
||
|
|
childrenMap.set(key, new Set(Array.from(children).map((n) => newGraph.getNode(n.id))));
|
||
|
|
});
|
||
|
|
newGraph.treeIndices.set(treeKey, {
|
||
|
|
parentMap: parentMap,
|
||
|
|
childrenMap: childrenMap,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
return newGraph;
|
||
|
|
}
|
||
|
|
toJSON() {
|
||
|
|
return JSON.stringify({
|
||
|
|
nodes: this.getAllNodes(),
|
||
|
|
edges: this.getAllEdges(),
|
||
|
|
// FIXME: And tree structures?
|
||
|
|
});
|
||
|
|
}
|
||
|
|
createView(options) {
|
||
|
|
return new GraphView({
|
||
|
|
graph: this,
|
||
|
|
...options,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=graph.js.map
|