trassel
Version:
Graph computing in JavaScript
105 lines (98 loc) • 3.4 kB
JavaScript
/**
* Takes a graph as input and returns an acyclic array of edges.
* Based on Eades, Lin, Smyth, '93
* @param {import("../../model/ibasicnode").IBasicNode[]} nodes
* @param {import("../../model/ibasicedge").IBasicEdge[]} edges
* @returns {import("../../model/ibasicedge").IBasicEdge[]} - Acyclic array of edges
*/
export const makeAcyclic = (nodes, edges) => {
const originalNodes = [...nodes]
const originalEdges = edges.filter(edge => edge.targetNode !== edge.sourceNode)
const incomingCount = new Map(originalNodes.map(node => [node.id, 0]))
const outgoingCount = new Map(originalNodes.map(node => [node.id, 0]))
originalEdges.forEach(edge => {
incomingCount.set(edge.targetNode, incomingCount.get(edge.targetNode) + 1)
outgoingCount.set(edge.sourceNode, outgoingCount.get(edge.sourceNode) + 1)
})
const resultingNodes = []
const resultingEdges = []
const findSinksOrSources = (isSource = false) => {
while (true) {
let hasChanged = false
for (let i = 0; i < originalNodes.length; i++) {
const node = originalNodes[i]
if (
(!isSource && !outgoingCount.get(node.id) && incomingCount.get(node.id)) ||
(isSource && outgoingCount.get(node.id) && !incomingCount.get(node.id))
) {
hasChanged = true
resultingNodes.push(originalNodes.splice(i, 1)[0])
i--
for (let j = 0; j < originalEdges.length; j++) {
const edge = originalEdges[j]
if ((!isSource && edge.targetNode === node.id) || (isSource && edge.sourceNode === node.id)) {
outgoingCount.set(edge.sourceNode, outgoingCount.get(edge.sourceNode) - 1)
incomingCount.set(edge.targetNode, incomingCount.get(edge.targetNode) - 1)
resultingEdges.push(originalEdges.splice(j, 1)[0])
j--
}
}
}
}
if (hasChanged) {
continue
}
break
}
}
//Keep going until all nodes have been assigned into the result
while (originalNodes.length) {
//Find sinks
findSinksOrSources(false)
//Find all isolated nodes
for (let i = 0; i < originalNodes.length; i++) {
const node = originalNodes[i]
if (!incomingCount.get(node.id) && !outgoingCount.get(node.id)) {
originalNodes.splice(i, 1)
resultingNodes.push(node)
i--
}
}
//Find sources
findSinksOrSources(true)
//If there are still edges, find the maximal value based on outgoing - incoming and add it but remove the outgoing edges.
if (originalNodes.length) {
const maximalNode = originalNodes.reduce(
(acc, node, index) => {
const value = outgoingCount.get(node.id) - incomingCount.get(node.id)
if (value > acc[1]) {
acc = [node, value, index]
}
return acc
},
[null, 0, -1]
)
if (maximalNode[0] === null) {
//It was a stalemate, grab a random node
maximalNode[0] = originalNodes[0]
maximalNode[2] = 0
}
for (let i = 0; i < originalEdges.length; i++) {
const edge = originalEdges[i]
if (edge.sourceNode === maximalNode[0].id) {
incomingCount.set(edge.targetNode, incomingCount.get(edge.targetNode) - 1)
resultingEdges.push(edge)
originalEdges.splice(i, 1)
i--
}
if (edge.targetNode === maximalNode[0].id) {
outgoingCount.set(edge.sourceNode, outgoingCount.get(edge.sourceNode) - 1)
originalEdges.splice(i, 1)
i--
}
resultingNodes.push(originalNodes.splice(maximalNode[2], 1)[0])
}
}
}
return resultingEdges
}