Skip to content

Commit

Permalink
Make tree immutable and add compose compiler metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
overpas committed Oct 19, 2024
1 parent ee4f853 commit 166523b
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 37 deletions.
8 changes: 8 additions & 0 deletions sample/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ dependencies {
detektPlugins(libs.compose.detekt.rules)
}

composeCompiler {
stabilityConfigurationFiles.add(project.layout.projectDirectory.file("stability.conf"))
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
}
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
compilerOptions {
jvmTarget = JvmTarget.fromTarget(properties["jvm.version"].toString())
Expand Down
Empty file added sample/shared/stability.conf
Empty file.
8 changes: 8 additions & 0 deletions treemap-chart-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.test.manifest)
}

composeCompiler {
stabilityConfigurationFiles.add(project.layout.projectDirectory.file("stability.conf"))
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
}
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
compilerOptions {
jvmTarget = JvmTarget.fromTarget(properties["jvm.version"].toString())
Expand Down
Empty file.
9 changes: 9 additions & 0 deletions treemap-chart/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ kotlin {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
api(libs.kotlinx.collections.immutable)
}
}
val desktopMain by getting {
Expand Down Expand Up @@ -95,6 +96,14 @@ tasks.withType<KotlinCompile> {
}
}

composeCompiler {
stabilityConfigurationFiles.add(project.layout.projectDirectory.file("stability.conf"))
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
}
}

tasks.register("commonUnitTest") {
dependsOn("testDebugUnitTest", "desktopTest", "iosSimulatorArm64Test")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package by.overpass.treemapchart.core.measure.squarified

import androidx.compose.runtime.Stable
import by.overpass.treemapchart.core.measure.LayoutOrientation
import by.overpass.treemapchart.core.measure.TreemapChartMeasurer
import by.overpass.treemapchart.core.measure.TreemapNode
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

/**
* An implementation of [TreemapChartMeasurer] that keeps the aspect ratio of the nodes
* as close to 1 as possible
* Uses the squarified treemap algorithm: http://www.win.tue.nl/~vanwijk/stm.pdf
* Implementation inspired by https://github.com/tasubo/javafx-chart-treemap
*/
@Stable
class SquarifiedMeasurer2 : TreemapChartMeasurer {

private var height = 0.0
private var width = 0.0
private var heightLeft = 0.0
private var widthLeft = 0.0
private var left = 0.0
private var top = 0.0
private var layoutOrientation = LayoutOrientation.VERTICAL

override fun measureNodes(values: List<Double>, width: Int, height: Int): List<TreemapNode> {
val children = mutableListOf<TreemapElement>()
setupSizeAndValues(children, width.toDouble(), height.toDouble(), values)
return measureNodes(children)
}

private fun setupSizeAndValues(
children: MutableList<TreemapElement>,
width: Double,
height: Double,
values: List<Double>,
) {
this.width = width
this.height = height
this.left = 0.0
this.top = 0.0
children.clear()
for (value in values) {
val treemapElement = TreemapElement(value)
children.add(treemapElement)
}
layoutOrientation = if (width > height) {
LayoutOrientation.VERTICAL
} else {
LayoutOrientation.HORIZONTAL
}
scaleArea(children)
}

private fun measureNodes(children: List<TreemapElement>): List<TreemapNode> {
val treemapNodeList: MutableList<TreemapNode> = ArrayList()
heightLeft = height
widthLeft = width
squarify(ArrayList(children), ArrayList(), minimumSide())
for (child in children) {
val treemapNode = TreemapNode(
child.width.toInt(),
child.height.toInt(),
child.left.toInt(),
child.top.toInt()
)
treemapNodeList.add(treemapNode)
check(child.top <= height) { "Top is bigger than height" }
check(child.left <= width) { "Left is bigger than width" }
}
return treemapNodeList
}

private fun squarify(children: List<TreemapElement>, row: List<TreemapElement>, w: Double) {
val remainPopped = ArrayDeque(children)
val c = remainPopped.removeFirst()
val concatRow = ArrayList(row)
concatRow.add(c)
val remaining = ArrayList(remainPopped)
val worstConcat = worst(concatRow, w)
val worstRow = worst(row, w)
if (row.isEmpty() || worstRow > worstConcat || isDoubleEqual(worstRow, worstConcat)) {
if (remaining.isEmpty()) {
layoutRow(concatRow, w)
} else {
squarify(remaining, concatRow, w)
}
} else {
layoutRow(row, w)
squarify(children, ArrayList(), minimumSide())
}
}

private fun worst(ch: List<TreemapElement>, w: Double): Double {
if (ch.isEmpty()) {
return Double.MAX_VALUE
}
var areaSum = 0.0
var maxArea = 0.0
var minArea = Double.MAX_VALUE
for (item in ch) {
val area = item.area
areaSum += area
minArea = min(minArea, area)
maxArea = max(maxArea, area)
}
val sqw = w * w
val sqAreaSum = areaSum * areaSum
return max(
sqw * maxArea / sqAreaSum,
sqAreaSum / (sqw * minArea)
)
}

private fun layoutRow(row: List<TreemapElement>, w: Double) {
var totalArea = 0.0
for (element in row) {
val area = element.area
totalArea += area
}
if (layoutOrientation == LayoutOrientation.VERTICAL) {
val rowWidth = totalArea / w
var topItem = 0.0
for (element in row) {
val area = element.area
val h = area / rowWidth
element.top = top + topItem
element.left = left
element.width = rowWidth
element.height = h
topItem += h
}
widthLeft -= rowWidth
//this.heightLeft -= w;
left += rowWidth
val minimumSide = minimumSide()
if (!isDoubleEqual(minimumSide, heightLeft)) {
changeLayout()
}
} else {
val rowHeight = totalArea / w
var rowLeft = 0.0
for (item in row) {
val area = item.area
val wi = area / rowHeight
item.top = top
item.left = left + rowLeft
item.height = rowHeight
item.width = wi
rowLeft += wi
}
//this.widthLeft -= rowHeight;
heightLeft -= rowHeight
top += rowHeight
val minimumSide = minimumSide()
if (!isDoubleEqual(minimumSide, widthLeft)) {
changeLayout()
}
}
}

private fun changeLayout() {
layoutOrientation =
if (layoutOrientation == LayoutOrientation.HORIZONTAL) {
LayoutOrientation.VERTICAL
} else {
LayoutOrientation.HORIZONTAL
}
}

private fun isDoubleEqual(one: Double, two: Double): Boolean {
val eps = 0.00001
return abs(one - two) < eps
}

private fun minimumSide(): Double {
return min(heightLeft, widthLeft)
}

private fun scaleArea(children: List<TreemapElement>) {
val areaGiven = width * height
var areaTotalTaken = 0.0
for (child in children) {
val area = child.area
areaTotalTaken += area
}
val ratio = areaTotalTaken / areaGiven
for (child in children) {
val area = child.area / ratio
child.area = area
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package by.overpass.treemapchart.core.measure.squarified

import androidx.compose.runtime.Stable

/**
* An intermediary object to represent a [by.overpass.treemapchart.core.measure.TreemapNode]
*/
internal class TreemapElement(var area: Double) {
var left = 0.0
var top = 0.0
var width = 0.0
var height = 0.0
}
@Stable
internal data class TreemapElement(
var area: Double,
var left: Double = 0.0,
var top: Double = 0.0,
var width: Double = 0.0,
var height: Double = 0.0,
)
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package by.overpass.treemapchart.core.tree

@Suppress("ForbiddenComment")
import androidx.compose.runtime.Stable
import kotlinx.collections.immutable.ImmutableList

/**
* Basic data structure to be used in treemap chart
* TODO: Fix stability
*
* @param root the root node
*/
@Stable
class Tree<T>(
val root: Node<T>,
) {
Expand All @@ -17,20 +19,11 @@ class Tree<T>(
* @param data value of the node
* @param children child nodes of the node
*/
class Node<T>(val data: T, children: List<Node<T>> = listOf()) {

private val internalChildren = children.toMutableList()
val children: List<Node<T>> get() = internalChildren


fun addChild(node: Node<T>) {
internalChildren += node
}

fun removeChild(node: Node<T>) {
internalChildren -= node
}
}
@Stable
class Node<T>(
val data: T,
val children: ImmutableList<Node<T>>,
)
}

fun <T> Tree<T>.dfs(): List<T> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package by.overpass.treemapchart.core.tree

import kotlinx.collections.immutable.toImmutableList

@DslMarker
annotation class TreeDslMarker

Expand All @@ -15,35 +17,36 @@ interface NodeDsl<in T> {
}

@TreeDslMarker
private class NodeDslImpl<T>(private val node: Tree.Node<T>) : NodeDsl<T> {
private class NodeDslImpl<T> : NodeDsl<T> {

private val childNodes = mutableListOf<NodeDslImpl<T>>()
private val nodes = mutableListOf<Tree.Node<T>>()

override fun node(value: T, nodeBuilder: NodeDsl<T>.() -> Unit) {
childNodes += NodeDslImpl(Tree.Node(value)).apply(nodeBuilder)
nodes += NodeDslImpl<T>()
.apply(nodeBuilder)
.build(value)
}

fun build(): Tree.Node<T> {
childNodes.forEach {
node.addChild(it.build())
}
return node
fun build(value: T): Tree.Node<T> {
return Tree.Node(value, nodes.toImmutableList())
}
}

@TreeDslMarker
interface TreeDsl<in T> : NodeDsl<T>

@TreeDslMarker
private class TreeDslImpl<T>(rootValue: T) : TreeDsl<T> {
private class TreeDslImpl<T>(
private val rootValue: T,
) : TreeDsl<T> {

private val rootNode = NodeDslImpl(Tree.Node(rootValue))
private val rootNode = NodeDslImpl<T>()

override fun node(value: T, nodeBuilder: NodeDsl<T>.() -> Unit) {
rootNode.node(value, nodeBuilder)
}

fun build(): Tree<T> = Tree(rootNode.build())
fun build(): Tree<T> = Tree(rootNode.build(rootValue))
}

@TreeDslMarker
Expand Down
Loading

0 comments on commit 166523b

Please sign in to comment.