All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.data2viz.hierarchy.PackLayout.kt Maven / Gradle / Ivy

There is a newer version: 0.8.0-RC5
Show newest version
package io.data2viz.hierarchy

import io.data2viz.hierarchy.pack.packEnclose
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt

interface CircleValues{
    var x: Double
    var y: Double
    var r: Double
}

data class PackNode(
    val data: D,
    var depth: Int,
    var height: Int,
    override var value: Double?,
    override val children: MutableList> = mutableListOf(),
    override var parent: PackNode? = null,
    override var x: Double = .0,
    override var y: Double = .0,
    override var r: Double = .0,
    var previous: PackNode? = null,
    var next: PackNode? = null
) : ParentValued>, Children>, CircleValues

fun  packNode(node: PackNode): PackNode = PackNode(
    node.data, node.depth, node.height, node.value, node.children, node.parent, node.x, node.y, node.r, node.previous, node.next
)

/**
 * Enclosure diagrams use containment (nesting) to represent a hierarchy.
 * The size of the leaf circles encodes a quantitative dimension of the data.
 * The enclosing circles show the approximate cumulative size of each subtree, but due to wasted space there is
 * some distortion; only the leaf nodes can be compared accurately. Although circle packing does not use space
 * as efficiently as a treemap, the “wasted” space more prominently reveals the hierarchical structure.
 */
class PackLayout {

    private val constantZero: (PackNode) -> Double = { .0 }
    private val defaultRadius: (PackNode) -> Double = { sqrt(it.value!!) }

    private var dx = 1.0
    private var dy = 1.0

    /**
     * If radius is specified, sets the pack layout’s radius accessor to the specified function.
     * If the radius accessor is null, the radius of each leaf circle is derived from the leaf node.value
     * (computed by node.sum); the radii are then scaled proportionally to fit the layout size.
     * If the radius accessor is not null, the radius of each leaf circle is specified exactly by the function.
     */
    var radius: ((PackNode) -> Double)? = null

    /**
     * If padding is specified, sets this pack layout’s padding accessor to the specified function.
     * When siblings are packed, tangent siblings will be separated by approximately the specified padding;
     * the enclosing parent circle will also be separated from its children by approximately the specified padding.
     *
     * If an explicit radius is not specified (null), the padding is approximate because a two-pass algorithm is
     * needed to fit within the layout size: the circles are first packed without padding; a scaling factor is computed
     * and applied to the specified padding; and lastly the circles are re-packed with padding.
     */
    var padding: (PackNode) -> Double = constantZero

    /**
     * Lays out the specified root hierarchy, assigning the following properties on root and its descendants:
     * - node.x - the x-coordinate of the circle’s center
     * - node.y - the y-coordinate of the circle’s center
     * - node.r - the radius of the circle
     *
     * You must call root.sum before passing the hierarchy to the pack layout.
     * You probably also want to call root.sort to order the hierarchy before computing the layout.
     */
    fun pack(root: Node): PackNode {

        val rootPack = makePack(root)

        rootPack.x = dx / 2
        rootPack.y = dy / 2
        if (radius != null) {
            rootPack.eachBefore(radiusLeaf(radius!!))
                .eachAfter(packChildren(padding, 0.5))
                .eachBefore(translateChild(1.0))
        } else {
            rootPack.eachBefore(radiusLeaf(defaultRadius))
                .eachAfter(packChildren(constantZero, 1.0))
                .eachAfter(packChildren(padding, rootPack.r / min(dx, dy)))
                .eachBefore(translateChild(min(dx, dy) / (2 * rootPack.r)))
        }
        return rootPack
    }

    fun size(width: Double, height: Double) {
        dx = width
        dy = height
    }

    /**
     * If radius is specified, sets the pack layout’s radius accessor to the specified function and returns this
     * pack layout.
     * If radius is not specified, returns the current radius accessor, which defaults to null.
     * If the radius accessor is null, the radius of each leaf circle is derived
     * from the leaf node.value (computed by node.sum); the radii are then scaled proportionally to fit the layout size.
     * If the radius accessor is not null, the radius of each leaf circle is specified exactly by the function.
     */
    fun radius(radius: ((PackNode) -> Double)?) {
        this.radius = radius
    }

    /**
     * If padding is specified, sets this pack layout’s padding accessor to the specified number or function
     * or returns this pack layout.
     * If padding is not specified, returns the current padding accessor, which defaults to the constant zero.
     * When siblings are packed, tangent siblings will be separated by approximately the specified padding;
     * the enclosing parent circle will also be separated from its children by approximately the specified padding.
     * If an explicit radius is not specified, the padding is approximate because a two-pass algorithm is needed
     * to fit within the layout size: the circles are first packed without padding; a scaling factor is computed
     * and applied to the specified padding; and lastly the circles are re-packed with padding.
     */
    fun padding(padding: (PackNode) -> Double) {
        this.padding = padding
    }

    // TODO check positive value ?
    private fun radiusLeaf(radius: ((PackNode) -> Double)): ((PackNode) -> Unit) {
        return { node: PackNode ->
            if (node.children.isEmpty()) {
                node.r = max(.0, radius(node))
            }
        }
    }

    private fun  makePack(root: Node): PackNode {
        val rootPack = PackNode(root.data, root.depth, root.height, root.value)
        val nodes = mutableListOf(root)
        val nodesP = mutableListOf(rootPack)
        while (nodes.isNotEmpty()) {
            val node = nodes.removeAt(nodes.lastIndex)
            val nodeP = nodesP.removeAt(nodesP.lastIndex)
            node.children.forEach { child ->
                val c = PackNode(child.data, child.depth, child.height, child.value)
                c.parent = nodeP
                nodeP.children.add(c)
                nodes.add(child)
                nodesP.add(c)
            }
        }
        return rootPack
    }

    private fun packChildren(padding: (PackNode) -> Double, k: Double): ((PackNode) -> Unit) {
        return { node: PackNode ->
            if (node.children.isNotEmpty()) {
                val children = node.children
                val r = padding(node) * k

                children.forEach { it.r += r }
                val e = packEnclose(children)
                children.forEach { it.r -= r }
                node.r = e + r
            }
        }
    }

    private fun translateChild(k: Double): ((PackNode) -> Unit) {
        return { node: PackNode ->
            val parent = node.parent
            node.r *= k
            if (parent != null) {
                node.x = parent.x + k * node.x
                node.y = parent.y + k * node.y
            }
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy