io.data2viz.hierarchy.TreemapLayout.kt Maven / Gradle / Ivy
package io.data2viz.hierarchy
import io.data2viz.hierarchy.treemap.treemapSquarify
import kotlin.math.roundToInt
data class Row(
override var value: Double?,
val dice:Boolean,
override val children:List>
) : ParentValued>
class TreemapNode(
val data: D,
var depth: Int,
var height: Int,
override var value: Double?, // TODO differentiate value and SUM
override val children: MutableList> = mutableListOf(),
override var parent: TreemapNode? = null,
var x0: Double = .0,
var y0: Double = .0,
var x1: Double = .0,
var y1: Double = .0
): ParentValued>, Children>
internal fun roundNode(node: TreemapNode<*>) {
node.x0 = node.x0.roundToInt().toDouble()
node.y0 = node.y0.roundToInt().toDouble()
node.x1 = node.x1.roundToInt().toDouble()
node.y1 = node.y1.roundToInt().toDouble()
}
internal fun makeTreemap(root: Node): TreemapNode {
val rootTreemap = TreemapNode(root.data, root.depth, root.height, root.value)
val nodes = mutableListOf(root)
val nodesTM = mutableListOf(rootTreemap)
while (nodes.isNotEmpty()) {
val node = nodes.removeAt(nodes.lastIndex)
val nodeTM = nodesTM.removeAt(nodesTM.lastIndex)
node.children.forEach { child ->
val c = TreemapNode(child.data, child.depth, child.height, child.value)
c.parent = nodeTM
nodeTM.children.add(c)
nodes.add(child)
nodesTM.add(c)
}
}
return rootTreemap
}
class TreemapLayout {
private val constantZero: (TreemapNode) -> Double = { .0 }
var tilingMethod: (ParentValued>, Double, Double, Double, Double) -> Any = {
parent: ParentValued>, x0: Double, y0: Double, x1: Double, y1: Double -> treemapSquarify(parent, x0, y0, x1, y1)
}
var round = false
var width = 1.0
var height = 1.0
private var paddingStack = mutableListOf(.0)
var paddingInner: (TreemapNode) -> Double = constantZero
var paddingTop: (TreemapNode) -> Double = constantZero
var paddingRight: (TreemapNode) -> Double = constantZero
var paddingBottom: (TreemapNode) -> Double = constantZero
var paddingLeft: (TreemapNode) -> Double = constantZero
var paddingOuter: (TreemapNode) -> Double = constantZero
set(value) {
paddingTop = value
paddingRight = value
paddingBottom = value
paddingLeft = value
}
/**
* Introduced by Ben Shneiderman in 1991, a treemap recursively subdivides area into rectangles according to
* each node’s associated value.
* Treemap implementation supports an extensible tiling method: the default squarified method seeks to generate
* rectangles with a golden aspect ratio; this offers better readability and size estimation than slice-and-dice,
* which simply alternates between horizontal and vertical subdivision by depth.
*
* Lays out the specified root hierarchy, assigning the following properties on root and its descendants:
*
* - node.x0 - the left edge of the rectangle
* - node.y0 - the top edge of the rectangle
* - node.x1 - the right edge of the rectangle
* - node.y1 - the bottom edge of the rectangle
*
* You must call root.sum before passing the hierarchy to the treemap layout so each node as a positive value.
* // TODO force a call on root.sum ?
* You probably also want to call root.sort to order the hierarchy before computing the layout.
*/
fun treemap(root: Node): TreemapNode {
// TODO : require a check on each node to verify that value != null and >0 ? (root.sum has been passed) ?
val rootTreemap = makeTreemap(root)
paddingStack = MutableList(root.height + 1, { .0 })
rootTreemap.x0 = .0
rootTreemap.y0 = .0
rootTreemap.x1 = width
rootTreemap.y1 = height
rootTreemap.eachBefore(this::positionNode)
paddingStack = mutableListOf(.0)
if (round) rootTreemap.eachBefore(::roundNode)
return rootTreemap
}
private fun positionNode(node: TreemapNode) {
var p = paddingStack[node.depth]
var x0 = node.x0 + p
var y0 = node.y0 + p
var x1 = node.x1 - p
var y1 = node.y1 - p
if (x1 < x0) {
val mid = (x0 + x1) / 2
x0 = mid
x1 = mid
}
if (y1 < y0) {
val mid = (y0 + y1) / 2
y0 = mid
y1 = mid
}
node.x0 = x0
node.y0 = y0
node.x1 = x1
node.y1 = y1
if (node.children.isNotEmpty()) {
paddingStack[node.depth + 1] = paddingInner(node) / 2
p = paddingInner(node) / 2
x0 += paddingLeft(node) - p
y0 += paddingTop(node) - p
x1 -= paddingRight(node) - p
y1 -= paddingBottom(node) - p
if (x1 < x0) {
val mid = (x0 + x1) / 2
x0 = mid
x1 = mid
}
if (y1 < y0) {
val mid = (y0 + y1) / 2
y0 = mid
y1 = mid
}
tilingMethod(node, x0, y0, x1, y1)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy