diff --git a/benchmark/.gitignore b/benchmark/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/benchmark/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts
new file mode 100644
index 0000000..4e334b6
--- /dev/null
+++ b/benchmark/build.gradle.kts
@@ -0,0 +1,54 @@
+plugins {
+ alias(libs.plugins.android.test)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "com.example.benchmark"
+ compileSdk = properties["android.compileSdk"].toString().toInt()
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.toVersion(properties["jvm.version"].toString())
+ targetCompatibility = JavaVersion.toVersion(properties["jvm.version"].toString())
+ }
+
+ kotlinOptions {
+ jvmTarget = properties["jvm.version"].toString()
+ }
+
+ defaultConfig {
+ minSdk = properties["android.minSdk"].toString().toInt()
+ targetSdk = properties["android.targetSdk"].toString().toInt()
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR"
+ }
+
+ buildTypes {
+ // This benchmark buildType is used for benchmarking, and should function like your
+ // release build (for example, with minification on). It"s signed with a debug key
+ // for easy local/CI testing.
+ create("benchmark") {
+ isDebuggable = true
+// isMinifyEnabled = true
+ signingConfig = getByName("debug").signingConfig
+ matchingFallbacks += listOf("release")
+ }
+ }
+
+ targetProjectPath = ":sample:android"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
+}
+
+dependencies {
+ implementation(libs.androidx.test.ext.junit)
+ implementation(libs.androidx.test.espresso.core)
+ implementation(libs.ui.automator)
+ implementation(libs.macrobenchmark.junit)
+}
+
+androidComponents {
+ beforeVariants(selector().all()) {
+ it.enable = it.buildType == "benchmark"
+ }
+}
\ No newline at end of file
diff --git a/benchmark/src/main/AndroidManifest.xml b/benchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..227314e
--- /dev/null
+++ b/benchmark/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/benchmark/src/main/java/com/example/benchmark/TreemapChartBenchmark.kt b/benchmark/src/main/java/com/example/benchmark/TreemapChartBenchmark.kt
new file mode 100644
index 0000000..ecfc76a
--- /dev/null
+++ b/benchmark/src/main/java/com/example/benchmark/TreemapChartBenchmark.kt
@@ -0,0 +1,103 @@
+package com.example.benchmark
+
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.MacrobenchmarkScope
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TreemapChartBenchmark {
+
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ @Test
+ fun showSimpleChart() = measureRepeated {
+ startActivityAndWait()
+
+ device.wait(Until.hasObject(By.text("4")), 1000)
+ device.wait(Until.hasObject(By.text("2")), 1000)
+ device.wait(Until.hasObject(By.text("1")), 1000)
+ }
+
+ @Test
+ fun showComplexChart() = benchmarkRule.measureRepeated(
+ packageName = "by.overpass.treemapchart.sample.android",
+ metrics = listOf(FrameTimingMetric()),
+ compilationMode = CompilationMode.DEFAULT,
+ startupMode = StartupMode.COLD,
+ iterations = 5,
+ ) {
+ startActivityAndWait()
+
+ device.findObject(By.text("Show more complex chart"))
+ .click()
+
+ device.wait(
+ Until.hasObject(
+ By.text(
+ """
+ |Cars
+ |12.11%
+ """.trimMargin(),
+ ),
+ ),
+ 5000,
+ )
+ }
+
+ @Test
+ fun showComplexChartDetailPopup() = benchmarkRule.measureRepeated(
+ packageName = "by.overpass.treemapchart.sample.android",
+ metrics = listOf(FrameTimingMetric()),
+ compilationMode = CompilationMode.DEFAULT,
+ startupMode = StartupMode.COLD,
+ iterations = 5,
+ ) {
+ startActivityAndWait()
+
+ device.findObject(By.text("Show more complex chart"))
+ .click()
+
+ device.wait(
+ Until.hasObject(
+ By.text(
+ """
+ |Cars
+ |12.11%
+ """.trimMargin(),
+ ),
+ ),
+ 5000,
+ )
+ device.findObject(
+ By.text(
+ """
+ |Cars
+ |12.11%
+ """.trimMargin(),
+ ),
+ ).click()
+
+ device.wait(Until.hasObject(By.text("Exports value")), 5000)
+ device.wait(Until.hasObject(By.text("$88.6B")), 5000)
+ }
+
+ private fun measureRepeated(block: MacrobenchmarkScope.() -> Unit) {
+ benchmarkRule.measureRepeated(
+ packageName = "by.overpass.treemapchart.sample.android",
+ metrics = listOf(FrameTimingMetric()),
+ compilationMode = CompilationMode.DEFAULT,
+ startupMode = StartupMode.COLD,
+ iterations = 5,
+ measureBlock = block,
+ )
+ }
+}
diff --git a/sample/android/build.gradle.kts b/sample/android/build.gradle.kts
index 6ab894d..7668007 100644
--- a/sample/android/build.gradle.kts
+++ b/sample/android/build.gradle.kts
@@ -31,7 +31,13 @@ android {
isDebuggable = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
-// create("benchmark") {
+ create("benchmark") {
+ initWith(buildTypes.getByName("release"))
+ signingConfig = signingConfigs.getByName("debug")
+ matchingFallbacks += listOf("release")
+ isDebuggable = false
+ }
+ // create("benchmark") {
// initWith(release)
// signingConfig = signingConfigs.getByName("debug")
// matchingFallbacks.add("release")
diff --git a/sample/android/src/main/AndroidManifest.xml b/sample/android/src/main/AndroidManifest.xml
index 47f7281..08fcce2 100644
--- a/sample/android/src/main/AndroidManifest.xml
+++ b/sample/android/src/main/AndroidManifest.xml
@@ -1,5 +1,6 @@
@@ -7,12 +8,16 @@
-
+
+
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index e5520ca..462afe2 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -23,7 +23,7 @@ include(":treemap-chart")
include(":treemap-chart-compose")
include(":sample:shared")
include(":sample:android")
-//include(":sample:android:macrobenchmark")
include(":sample:desktop")
include(":sample:web")
include(":sample:web-wasm")
+include(":benchmark")
diff --git a/treemap-chart/src/commonMain/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurer2.kt b/treemap-chart/src/commonMain/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurer2.kt
deleted file mode 100644
index 7ea1d78..0000000
--- a/treemap-chart/src/commonMain/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurer2.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-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, width: Int, height: Int): List {
- val children = mutableListOf()
- setupSizeAndValues(children, width.toDouble(), height.toDouble(), values)
- return measureNodes(children)
- }
-
- private fun setupSizeAndValues(
- children: MutableList,
- width: Double,
- height: Double,
- values: List,
- ) {
- 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): List {
- val treemapNodeList: MutableList = 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, row: List, 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, 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, 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) {
- 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
- }
- }
-}
diff --git a/treemap-chart/src/commonTest/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurerTest.kt b/treemap-chart/src/commonTest/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurerTest.kt
index 6653c03..b5fcc71 100644
--- a/treemap-chart/src/commonTest/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurerTest.kt
+++ b/treemap-chart/src/commonTest/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurerTest.kt
@@ -8,7 +8,6 @@ import kotlin.time.measureTimedValue
class SquarifiedMeasurerTest {
private val squarifiedMeasurer = SquarifiedMeasurer()
- private val squarifiedMeasurer2 = SquarifiedMeasurer2()
private val values = listOf(6.0, 6.0, 4.0, 3.0, 2.0, 2.0, 1.0)
@Test
@@ -50,44 +49,4 @@ class SquarifiedMeasurerTest {
assertEquals(expectedNodes, actual.value)
println("Time: ${actual.duration}")
}
-
- @Test
- fun nodesAreMeasuredCorrectlyWithSquarifiedAlgorithmVerticalScreen2() {
- val expectedNodes = listOf(
- TreemapNode(width = 540, height = 960, offsetX = 0, offsetY = 0),
- TreemapNode(width = 540, height = 960, offsetX = 540, offsetY = 0),
- TreemapNode(width = 630, height = 548, offsetX = 0, offsetY = 960),
- TreemapNode(width = 630, height = 411, offsetX = 0, offsetY = 1508),
- TreemapNode(width = 450, height = 384, offsetX = 630, offsetY = 960),
- TreemapNode(width = 450, height = 384, offsetX = 630, offsetY = 1344),
- TreemapNode(width = 450, height = 192, offsetX = 630, offsetY = 1728),
- )
-
- val actual = measureTimedValue {
- squarifiedMeasurer2.measureNodes(values, 1080, 1920)
- }
-
- assertEquals(expectedNodes, actual.value)
- println("Time: ${actual.duration}")
- }
-
- @Test
- fun nodesAreMeasuredCorrectlyWithSquarifiedAlgorithmHorizontalScreen2() {
- val expectedNodes = listOf(
- TreemapNode(width = 960, height = 540, offsetX = 0, offsetY = 0),
- TreemapNode(width = 960, height = 540, offsetX = 0, offsetY = 540),
- TreemapNode(width = 548, height = 630, offsetX = 960, offsetY = 0),
- TreemapNode(width = 411, height = 630, offsetX = 1508, offsetY = 0),
- TreemapNode(width = 384, height = 450, offsetX = 960, offsetY = 630),
- TreemapNode(width = 384, height = 450, offsetX = 1344, offsetY = 630),
- TreemapNode(width = 192, height = 450, offsetX = 1728, offsetY = 630),
- )
-
- val actual = measureTimedValue {
- squarifiedMeasurer2.measureNodes(values, 1920, 1080)
- }
-
- assertEquals(expectedNodes, actual.value)
- println("Time: ${actual.duration}")
- }
}