io.data2viz.hierarchy.ClusterLayout.kt Maven / Gradle / Ivy
package io.data2viz.hierarchy
data class ClusterNode(
val data: D,
var depth: Int,
var height: Int,
override var value: Double?,
override val children: MutableList> = mutableListOf(),
override var parent: ClusterNode? = null,
var x: Double = .0,
var y: Double = .0
): ParentValued>, Children>
class ClusterLayout {
var nodeSize = false
private var dx = 1.0
private var dy = 1.0
/**
* Lays out the specified root hierarchy, assigning the following properties on root and its descendants:
* node.x - the x-coordinate of the node
* node.y - the y-coordinate of the node
*
* The coordinates x and y represent an arbitrary coordinate system; for example, you can treat x as an angle
* and y as a radius to produce a radial layout.
* You may want to call root.sort before passing the hierarchy to the cluster layout.
*
* The cluster layout produces dendrograms: node-link diagrams that place leaf nodes of the tree at the same depth.
* Dendograms are typically less compact than tidy trees, but are useful when all the leaves should be at
* the same level, such as for hierarchical clustering or phylogenetic tree diagrams.
*/
fun cluster(root: Node): ClusterNode {
val rootCluster = makeCluster(root)
var previousNode: ClusterNode? = null
var x = .0
// First walk, computing the initial x & y values
rootCluster.eachAfter({ node: ClusterNode ->
val children = node.children
if (children.isNotEmpty()) {
node.x = children.sumByDouble { it.x } / children.size
node.y = children.maxBy { it.y }!!.y + 1
} else {
if (previousNode != null) {
x += separation(node, previousNode!!)
node.x = x
} else node.x = .0
node.y = .0
previousNode = node
}
})
val left = leafLeft(rootCluster)
val right = leafRight(rootCluster)
val x0 = left.x - separation(left, right) / 2
val x1 = right.x + separation(right, left) / 2
// Second walk, normalizing x & y to the desired size.
return if (nodeSize) {
rootCluster.eachAfter({ node: ClusterNode ->
node.x = (node.x - rootCluster.x) * dx
node.y = (rootCluster.y - node.y) * dy
})
} else {
rootCluster.eachAfter({ node: ClusterNode ->
node.x = (node.x - x0) / (x1 - x0) * dx
node.y = if (rootCluster.y == .0) .0 else (1 - (node.y / rootCluster.y)) * dy
})
}
}
fun size(width: Double, height: Double) {
nodeSize = false
dx = width
dy = height
}
fun nodeSize(width: Double, height: Double) {
nodeSize = true
dx = width
dy = height
}
private fun makeCluster(root: Node): ClusterNode {
val rootCluster = ClusterNode(root.data, root.depth, root.height, root.value)
val nodes = mutableListOf(root)
val nodesC = mutableListOf(rootCluster)
while (nodes.isNotEmpty()) {
val node = nodes.removeAt(nodes.lastIndex)
val nodeC = nodesC.removeAt(nodesC.lastIndex)
node.children.forEach { child ->
val c = ClusterNode(child.data, child.depth, child.height, child.value)
c.parent = nodeC
nodeC.children.add(c)
nodes.add(child)
nodesC.add(c)
}
}
return rootCluster
}
private fun leafLeft(node: ClusterNode): ClusterNode {
var children = node.children
var current: ClusterNode = node
while (children.isNotEmpty()) {
current = children[0]
children = current.children
}
return current
}
private fun leafRight(node: ClusterNode): ClusterNode {
var children = node.children
var current: ClusterNode = node
while (children.isNotEmpty()) {
current = children[children.lastIndex]
children = current.children
}
return current
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy