Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 43 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@

![Tree Layout KMP Visualization Example](images/output.gif)

A pure **Kotlin Multiplatform** library for computing tidy, aesthetically pleasing tree visualizations. It implements
the Walker algorithm (Buchheim–Jünger–Leipert variant) in $O(n)$ time with zero platform dependencies — no JVM, Android,
or iOS frameworks required.
A pure **Kotlin Multiplatform** library for computing tidy, aesthetically pleasing tree visualizations. It implements the [Walker algorithm (Buchheim–Jünger–Leipert variant)](https://link.springer.com/chapter/10.1007/3-540-36151-0_32) in $O(n)$ time with zero platform dependencies — no JVM, Android, or iOS frameworks required.

TreeLayoutKMP works with *any* tree structure you already have. You provide a thin adapter describing how to traverse
`TreeLayoutKMP` works with *any* tree structure you already have. You provide a thin adapter describing how to traverse
your nodes; the library computes optimal (x, y) coordinates for every node in the tree.

> **Note:** This library is a *layout engine*, not a rendering library. It outputs coordinates — how you draw the tree
> (Canvas, SVG, Compose, HTML, etc.) is entirely up to you.

**[Try the live demo on GitHub Pages](https://linde9821.github.io/TreeLayoutKMP/)**

## Supported Targets

| JVM | Android | iOS | Linux x64 | wasmJs | js |
Expand Down Expand Up @@ -139,7 +142,7 @@ The layout engine uses node extents to compute center-to-center distances:

### 5. Layout Orientation

By default the tree grows top-to-bottom. Use the `orientation` property to change direction:
By default, the tree grows top-to-bottom. Use the `orientation` property to change direction:

```kotlin
val config = WalkerLayoutConfiguration(
Expand Down Expand Up @@ -221,28 +224,55 @@ The layout is computed using the Buchheim–Jünger–Leipert improvement of the
as possible while preserving symmetry.
- **Deterministic output** — the same tree always produces the same coordinates.

## Benchmark

The `benchmark/` module measures layout computation time across trees ranging from 1 to ~6 million nodes. Results
confirm the algorithm's **O(n) linear time complexity** — computation time scales proportionally with node count.

![Benchmark Results](benchmark/lets-plot-images/benchmark_results.png)

Run the benchmark yourself:

```bash
./gradlew :benchmark:jvmRun
```

## Running the Samples

### Compose Desktop (interactive visualization)
The `sample/` module is a Compose Multiplatform application that visualizes a **prefix tree** built from user-provided
words. It demonstrates variable node sizes, all four orientations, and interactive layout controls.

The `sample/` module contains a Compose Desktop application that renders an interactive tree visualization using the
library.
### Desktop (JVM)

```bash
./gradlew :sample:run
```

This opens a window displaying an org-chart tree with variable node sizes and edges drawn between parent and child
nodes.
Opens a window with a side panel for layout controls, a text input for words, and a zoomable tree canvas.

### Web (wasmJs)

```bash
./gradlew :sample:wasmJsBrowserRun
```

Launches a local dev server and opens the sample in your browser. This is the same app deployed to GitHub Pages.

### JVM CLI (ASCII + PNG export)
### Android

The library module includes a JVM sample that renders an ASCII visualization to the console and exports a PNG image.
The standalone Android sample lives in `sample/android/`. Open it in Android Studio and run on a device or emulator, or
build from the command line:

```bash
./gradlew :library:runSample
cd sample/android
./gradlew installDebug
```

### iOS

Open `sample/iosApp/iosApp.xcodeproj` in Xcode and run on a simulator or device. The shared Kotlin code is compiled
into a static framework via the `:sample` module's iOS targets.

## License

Apache License 2.0 — see [LICENSE](LICENSE) for details.
4 changes: 3 additions & 1 deletion benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Performance benchmark for the TreeLayoutKMP library.

## What it does

Generates balanced trees with increasing node counts (stepping by 15K up to ~1.1M) and measures the time to compute a full layout for each. Results are plotted as a line chart and exported to `benchmark_results.png` using Kotlin's lets-plot library.
Generates balanced trees with increasing node counts (stepping by 15K up to ~1.1M) and measures the time to compute a
full layout for each. Results are plotted as a line chart and exported to `benchmark_results.png` using Kotlin's
lets-plot library.

## Output

Expand Down
2 changes: 1 addition & 1 deletion benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ tasks.register<JavaExec>("runBenchmark") {
description = "Run the TreeLayoutKMP benchmark"
mainClass.set("io.github.linde9821.treelayout.benchmark.MainKt")
classpath = kotlin.jvm().compilations["main"].runtimeDependencyFiles +
kotlin.jvm().compilations["main"].output.allOutputs
kotlin.jvm().compilations["main"].output.allOutputs
jvmArgs = listOf("-Xmx8g", "-XX:+UseG1GC", "-XX:+AlwaysPreTouch")
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import org.jetbrains.letsPlot.label.ylab
import org.jetbrains.letsPlot.letsPlot
import org.jetbrains.letsPlot.scale.scaleColorManual
import org.jetbrains.letsPlot.scale.scaleLinetypeManual
import org.jetbrains.letsPlot.scale.scaleSizeManual
import org.jetbrains.letsPlot.themes.themeMinimal
import kotlin.time.measureTime

Expand Down Expand Up @@ -123,16 +122,16 @@ fun main() {
val os = System.getProperty("os.name")

val plot = letsPlot(data) { x = "nodes"; y = "time_ms" } +
geomLine(data = referenceData) { color = "series"; linetype = "series" } +
geomLine(data = measuredData) { color = "series"; linetype = "series" } +
geomPoint(data = measuredData, size = 1.5) { color = "series" } +
scaleColorManual(values = listOf("#999999", "#2166AC")) +
scaleLinetypeManual(values = listOf("dashed", "solid")) +
xlab("Number of Nodes") +
ylab("Time (ms)") +
labs(subtitle = "JVM $jvmVersion · $os") +
ggtitle("TreeLayoutKMP Benchmark") +
themeMinimal()
geomLine(data = referenceData) { color = "series"; linetype = "series" } +
geomLine(data = measuredData) { color = "series"; linetype = "series" } +
geomPoint(data = measuredData, size = 1.5) { color = "series" } +
scaleColorManual(values = listOf("#999999", "#2166AC")) +
scaleLinetypeManual(values = listOf("dashed", "solid")) +
xlab("Number of Nodes") +
ylab("Time (ms)") +
labs(subtitle = "JVM $jvmVersion · $os") +
ggtitle("TreeLayoutKMP Benchmark") +
themeMinimal()

ggsave(plot, "benchmark_results.png", dpi = 150, w = 10.0, h = 6.0)
println("\nChart written to benchmark_results.png")
Expand Down
10 changes: 0 additions & 10 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@file:OptIn(ExperimentalWasmDsl::class)

import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

Expand Down Expand Up @@ -104,12 +103,3 @@ mavenPublishing {
}
}
}

tasks.register<JavaExec>("runSample") {
description = "Runs the JVM sample application demonstrating tree layout"
val jvmJar = tasks.named("jvmJar")
dependsOn(jvmJar)
val runtimeClasspath = kotlin.jvm().compilations["main"].runtimeDependencyFiles
classpath = files(jvmJar.map { (it as Jar).archiveFile }, runtimeClasspath)
mainClass.set("io.github.linde9821.treelayout.sample.SampleAppKt")
}

This file was deleted.

This file was deleted.

Loading
Loading