Improved layouting (minimize #crossings)
This commit is contained in:
@ -14,6 +14,15 @@ import kotlin.math.max
|
|||||||
* This algorithm arranges the graph in layers, attempting to minimize edge crossings and distribute nodes evenly.
|
* This algorithm arranges the graph in layers, attempting to minimize edge crossings and distribute nodes evenly.
|
||||||
* It treats the graph as a collection of disjoint acyclic graphs, disregarding back-edges during layout.
|
* It treats the graph as a collection of disjoint acyclic graphs, disregarding back-edges during layout.
|
||||||
*
|
*
|
||||||
|
* You can improve the layout by increasing the iteration count via [setIterationCount], at the cost of longer
|
||||||
|
* computation time (default: 3).
|
||||||
|
*
|
||||||
|
* Additionally, you can ignore certain edges during layout by providing a function to [ignoreInLayout]; during each
|
||||||
|
* of the phases where edges can be ignored ([LayoutPhase.LAYERING] (deciding layers for each vertex),
|
||||||
|
* [LayoutPhase.DISJOINTS] (deciding which sub-graphs to consider disjoint), and [LayoutPhase.SLOT_ASSIGNMENT] (deciding
|
||||||
|
* the order of vertices in each layer)), the function will be called for each edge (once), and if it returns true,
|
||||||
|
* the edge will be ignored for that phase. By default, no edges are ignored.
|
||||||
|
*
|
||||||
* This layout algorithm does not support freezing vertices; calling [freezeAt] will throw
|
* This layout algorithm does not support freezing vertices; calling [freezeAt] will throw
|
||||||
* [UnsupportedOperationException], and [unfreeze] is a no-op.
|
* [UnsupportedOperationException], and [unfreeze] is a no-op.
|
||||||
* Additionally, [setGraph] and [setVertexSize] do not invalidate the cache.
|
* Additionally, [setGraph] and [setVertexSize] do not invalidate the cache.
|
||||||
@ -99,6 +108,7 @@ class PseudoForestLayout<V, E>(
|
|||||||
private val _positions = mutableMapOf<V, GPoint>()
|
private val _positions = mutableMapOf<V, GPoint>()
|
||||||
private var _vertexSize: (V) -> VertexSize = vertexSize
|
private var _vertexSize: (V) -> VertexSize = vertexSize
|
||||||
private var _boundingBox: GSize? = null
|
private var _boundingBox: GSize? = null
|
||||||
|
private var _repeat: Int = 3
|
||||||
|
|
||||||
@OptIn(ExperimentalAtomicApi::class)
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
private var _lock: AtomicBoolean = AtomicBoolean(false)
|
private var _lock: AtomicBoolean = AtomicBoolean(false)
|
||||||
@ -124,6 +134,20 @@ class PseudoForestLayout<V, E>(
|
|||||||
override fun graph(): IGraph<V, E> = _graph
|
override fun graph(): IGraph<V, E> = _graph
|
||||||
override fun setGraph(graph: IGraph<V, E>) { locked { _graph = graph } }
|
override fun setGraph(graph: IGraph<V, E>) { locked { _graph = graph } }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of iterations the layout algorithm will perform to optimize the layout.
|
||||||
|
*
|
||||||
|
* @return the number of iterations
|
||||||
|
*/
|
||||||
|
fun getIterationCount(): Int = _repeat
|
||||||
|
/**
|
||||||
|
* Sets the number of iterations the layout algorithm will perform to optimize the layout.
|
||||||
|
* More iterations may yield a better layout, but will take longer to compute.
|
||||||
|
*
|
||||||
|
* @param repeat the number of iterations
|
||||||
|
*/
|
||||||
|
fun setIterationCount(repeat: Int) { locked { _repeat = repeat } }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the vertex measuring function.
|
* Sets the vertex measuring function.
|
||||||
*
|
*
|
||||||
@ -156,7 +180,6 @@ class PseudoForestLayout<V, E>(
|
|||||||
|
|
||||||
fun directParentOf(other: Vert<V>, graph: IGraph<V, *>): Boolean = x.fold({ it1 ->
|
fun directParentOf(other: Vert<V>, graph: IGraph<V, *>): Boolean = x.fold({ it1 ->
|
||||||
other.x.fold({ it2 -> graph.xToY(it1, it2) != null }) { it2 ->
|
other.x.fold({ it2 -> graph.xToY(it1, it2) != null }) { it2 ->
|
||||||
println(" - Checking direct parenthood between $it1 and dummy $it2 (${it2.from}): ${it2.from?.x == it1}")
|
|
||||||
it2.from?.x == it1
|
it2.from?.x == it1
|
||||||
}
|
}
|
||||||
}) { it1 ->
|
}) { it1 ->
|
||||||
@ -176,8 +199,6 @@ class PseudoForestLayout<V, E>(
|
|||||||
if(i == chain.size - 1) (v.x.asT2).to = last
|
if(i == chain.size - 1) (v.x.asT2).to = last
|
||||||
else (v.x.asT2).to = chain[i + 1]
|
else (v.x.asT2).to = chain[i + 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
println(" - Breaking edge $from -> $to with chain: $chain")
|
|
||||||
return chain
|
return chain
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,17 +222,10 @@ class PseudoForestLayout<V, E>(
|
|||||||
|
|
||||||
private fun computeCrossings(lTop: List<Vert<V>>, lBot: List<Vert<V>>, edges: List<Pair<Vert<V>, Vert<V>>>): Int {
|
private fun computeCrossings(lTop: List<Vert<V>>, lBot: List<Vert<V>>, edges: List<Pair<Vert<V>, Vert<V>>>): Int {
|
||||||
var count = 0
|
var count = 0
|
||||||
|
|
||||||
println(" - Computing crossings between layers:")
|
|
||||||
println(" - Top: $lTop")
|
|
||||||
println(" - Bot: $lBot")
|
|
||||||
println(" - Edges: $edges")
|
|
||||||
|
|
||||||
val conns = edges.map { (top, bot) -> lTop.indexOf(top) to lBot.indexOf(bot) }.sortedWith { (t1, b1), (t2, b2) ->
|
val conns = edges.map { (top, bot) -> lTop.indexOf(top) to lBot.indexOf(bot) }.sortedWith { (t1, b1), (t2, b2) ->
|
||||||
if (t1 != t2) t1 - t2
|
if (t1 != t2) t1 - t2
|
||||||
else b1 - b2
|
else b1 - b2
|
||||||
}
|
}
|
||||||
println(" - Connections (by index): $conns")
|
|
||||||
|
|
||||||
conns.forEachIndexed { i, conn ->
|
conns.forEachIndexed { i, conn ->
|
||||||
for(j in i + 1 until conns.size) {
|
for(j in i + 1 until conns.size) {
|
||||||
@ -222,10 +236,7 @@ class PseudoForestLayout<V, E>(
|
|||||||
val topFirst = conn.first < other.first
|
val topFirst = conn.first < other.first
|
||||||
val botFirst = conn.second < other.second
|
val botFirst = conn.second < other.second
|
||||||
|
|
||||||
if(topFirst != botFirst) {
|
if(topFirst != botFirst) count++
|
||||||
println(" - Crossing between ${lTop[conn.first]}->${lBot[conn.second]} and ${lTop[other.first]}->${lBot[other.second]}")
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
@ -246,24 +257,20 @@ class PseudoForestLayout<V, E>(
|
|||||||
val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>()
|
val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>()
|
||||||
val queue = ArrayDeque<Pair<V, Set<V>>>(roots.size * 2)
|
val queue = ArrayDeque<Pair<V, Set<V>>>(roots.size * 2)
|
||||||
queue.addAll(roots.map { it to emptySet() })
|
queue.addAll(roots.map { it to emptySet() })
|
||||||
println(" - Computing layers from roots: $roots")
|
|
||||||
|
|
||||||
while (!queue.isEmpty()) {
|
while (!queue.isEmpty()) {
|
||||||
val (vertex, onPath) = queue.removeFirst()
|
val (vertex, onPath) = queue.removeFirst()
|
||||||
val (layer, dep) = layers.getOrPut(vertex) { 0 to mutableSetOf() }
|
val (layer, dep) = layers.getOrPut(vertex) { 0 to mutableSetOf() }
|
||||||
println(" - Visiting $vertex (layer $layer), path=$onPath, deps=$dep")
|
|
||||||
|
|
||||||
val succLayer = layer + 1
|
val succLayer = layer + 1
|
||||||
_graph.successors(vertex).forEach { (succ, edge) ->
|
_graph.successors(vertex).forEach { (succ, edge) ->
|
||||||
if(ignoreInLayout(edge, LayoutPhase.LAYERING)) {
|
if(ignoreInLayout(edge, LayoutPhase.LAYERING)) {
|
||||||
println(" - Ignoring edge $edge for layout")
|
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
if (succ in onPath) return@forEach
|
if (succ in onPath) return@forEach
|
||||||
dep += succ
|
dep += succ
|
||||||
|
|
||||||
layers[succ]?.let { (l, sDep) ->
|
layers[succ]?.let { (l, sDep) ->
|
||||||
println(" - Successor $succ already had layer $l (might be increased, along with its dependents)")
|
|
||||||
dep += sDep
|
dep += sDep
|
||||||
|
|
||||||
val delta = succLayer - l
|
val delta = succLayer - l
|
||||||
@ -277,15 +284,12 @@ class PseudoForestLayout<V, E>(
|
|||||||
} ?: run {
|
} ?: run {
|
||||||
layers[succ] = succLayer to mutableSetOf()
|
layers[succ] = succLayer to mutableSetOf()
|
||||||
}
|
}
|
||||||
println(" - Adding successor to queue: $succ (layer: ${layers[succ]?.first})")
|
|
||||||
queue.addLast(succ to onPath + vertex)
|
queue.addLast(succ to onPath + vertex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure dependents are always up to date
|
// ensure dependents are always up to date
|
||||||
layers.values.filter { it.second.contains(vertex) }.forEach { (_, d) -> d.addAll(dep) }
|
layers.values.filter { it.second.contains(vertex) }.forEach { (_, d) -> d.addAll(dep) }
|
||||||
}
|
}
|
||||||
println(" - Assigned layers:")
|
|
||||||
layers.forEach { (v, p) -> println(" - Vertex $v: layer ${p.first}, dependents: ${p.second}") }
|
|
||||||
|
|
||||||
// Cache node sizes
|
// Cache node sizes
|
||||||
val vertexSizes = layers.mapValues { (v, _) -> _vertexSize(v).let { it.fullSize() to it.vCenterInBox() } }
|
val vertexSizes = layers.mapValues { (v, _) -> _vertexSize(v).let { it.fullSize() to it.vCenterInBox() } }
|
||||||
@ -293,7 +297,6 @@ class PseudoForestLayout<V, E>(
|
|||||||
// Compute layer y positions (and thus the bounding box height).
|
// Compute layer y positions (and thus the bounding box height).
|
||||||
val layerCount = layers.maxOf { it.value.first } + 1
|
val layerCount = layers.maxOf { it.value.first } + 1
|
||||||
val layerHeights = MutableList(layerCount) { 0.0f }
|
val layerHeights = MutableList(layerCount) { 0.0f }
|
||||||
println(" - Have $layerCount layers")
|
|
||||||
|
|
||||||
var minOffset = Float.POSITIVE_INFINITY
|
var minOffset = Float.POSITIVE_INFINITY
|
||||||
var maxOffset = Float.NEGATIVE_INFINITY
|
var maxOffset = Float.NEGATIVE_INFINITY
|
||||||
@ -316,13 +319,6 @@ class PseudoForestLayout<V, E>(
|
|||||||
y
|
y
|
||||||
}
|
}
|
||||||
|
|
||||||
println(" - Layer measurements: (height, y) = ${(layerHeights zip layerY)}")
|
|
||||||
println(" - Layers with nodes:")
|
|
||||||
for(i in 0 until layerCount) {
|
|
||||||
val verts = layers.filter { it.value.first == i }.keys
|
|
||||||
println(" - Layer $i: $verts")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute disjoint graphs
|
// Compute disjoint graphs
|
||||||
val disjoint = roots.fold(listOf<Set<V>>()) { acc, root ->
|
val disjoint = roots.fold(listOf<Set<V>>()) { acc, root ->
|
||||||
val reachable = reachableFrom(root, LayoutPhase.DISJOINTS).toMutableSet()
|
val reachable = reachableFrom(root, LayoutPhase.DISJOINTS).toMutableSet()
|
||||||
@ -340,7 +336,6 @@ class PseudoForestLayout<V, E>(
|
|||||||
}
|
}
|
||||||
var currentXZero = 0.0f
|
var currentXZero = 0.0f
|
||||||
disjoint.forEach { sub ->
|
disjoint.forEach { sub ->
|
||||||
println(" - Layouting disjoint subgraph: $sub")
|
|
||||||
// Put each vertex in a list by layer
|
// Put each vertex in a list by layer
|
||||||
val layered = List(layerCount) { layer ->
|
val layered = List(layerCount) { layer ->
|
||||||
sub.mapNotNull {
|
sub.mapNotNull {
|
||||||
@ -349,9 +344,6 @@ class PseudoForestLayout<V, E>(
|
|||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
println(" - Initial layered vertices:")
|
|
||||||
layered.forEachIndexed { idx, list -> println(" - Layer $idx: $list") }
|
|
||||||
|
|
||||||
// Break up multi-layer edges with dummy nodes
|
// Break up multi-layer edges with dummy nodes
|
||||||
layered.forEachIndexed { idx, list ->
|
layered.forEachIndexed { idx, list ->
|
||||||
list.forEach { v ->
|
list.forEach { v ->
|
||||||
@ -413,22 +405,41 @@ class PseudoForestLayout<V, E>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val edges = mutableListOf<MutableList<Pair<Vert<V>, Vert<V>>>>()
|
val edges = mutableListOf<MutableList<Pair<Vert<V>, Vert<V>>>>()
|
||||||
val crossings = mutableListOf<Int>()
|
|
||||||
for(i in 1 until layered.size) {
|
for(i in 1 until layered.size) {
|
||||||
edges.add(mutableListOf())
|
edges.add(mutableListOf())
|
||||||
val current = edges[i - 1]
|
val current = edges[i - 1]
|
||||||
|
|
||||||
layered[i].forEach { vBot ->
|
layered[i].forEach { vBot ->
|
||||||
val forVBot = layered[i - 1].filter {
|
val forVBot = layered[i - 1].filter {
|
||||||
println(" - Checking edge between $it and $vBot: ${it.directParentOf(vBot, _graph)} || ${vBot.directParentOf(it, _graph)}")
|
|
||||||
it.directParentOf(vBot, _graph) || vBot.directParentOf(it, _graph)
|
it.directParentOf(vBot, _graph) || vBot.directParentOf(it, _graph)
|
||||||
}.map { it to vBot }
|
}.map { it to vBot }
|
||||||
current.addAll(forVBot)
|
current.addAll(forVBot)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val c = computeCrossings(layered[i - 1], layered[i], current)
|
repeat(_repeat) {
|
||||||
crossings.add(c)
|
val optP0 = layered[0].permutations().map { perm ->
|
||||||
println(" - Connections between layer ${i - 1} and $i have $c crossings")
|
val crossings = computeCrossings(perm, layered[1], edges[0])
|
||||||
|
println(" - Layer 0 permutation $perm (to ${layered[1]} has $crossings crossings")
|
||||||
|
perm to crossings
|
||||||
|
}.fold(Float.POSITIVE_INFINITY to listOf<Vert<V>>()) { (accCost, accSeq), (perm, cost) ->
|
||||||
|
if (accCost > cost) cost.toFloat() to perm
|
||||||
|
else accCost to accSeq
|
||||||
|
}
|
||||||
|
layered[0].clear()
|
||||||
|
layered[0].addAll(optP0.second.toMutableList())
|
||||||
|
for (i in 1 until layered.size) {
|
||||||
|
val optPi = layered[i].permutations().map { perm ->
|
||||||
|
val crossings = computeCrossings(layered[i - 1], perm, edges[i - 1])
|
||||||
|
println(" - Layer $i permutation $perm (from ${layered[i - 1]} has $crossings crossings")
|
||||||
|
perm to crossings
|
||||||
|
}.fold(Float.POSITIVE_INFINITY to listOf<Vert<V>>()) { (accCost, accSeq), (perm, cost) ->
|
||||||
|
if (accCost > cost) cost.toFloat() to perm
|
||||||
|
else accCost to accSeq
|
||||||
|
}
|
||||||
|
layered[i].clear()
|
||||||
|
layered[i].addAll(optPi.second.toMutableList())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign x positions
|
// Assign x positions
|
||||||
|
13
src/main/kotlin/com/jaytux/altgraph/layout/Util.kt
Normal file
13
src/main/kotlin/com/jaytux/altgraph/layout/Util.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package com.jaytux.altgraph.layout
|
||||||
|
|
||||||
|
fun <T> List<T>.permutations(): Sequence<List<T>> = sequence {
|
||||||
|
if(isEmpty()) yield(emptyList())
|
||||||
|
else {
|
||||||
|
for(i in indices) {
|
||||||
|
val elem = this@permutations[i]
|
||||||
|
for(perm in (this@permutations - elem).permutations()) {
|
||||||
|
yield(listOf(elem) + perm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user