Skip to content

Commit ba42330

Browse files
authored
Merge pull request #50 from joreilly/tree_map
add treechart for ios
2 parents 9fee3db + 1f70305 commit ba42330

File tree

4 files changed

+212
-0
lines changed

4 files changed

+212
-0
lines changed

composeApp/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ kotlin {
7777

7878
implementation(libs.ktor.client.android)
7979
implementation(libs.voyager)
80+
81+
implementation("io.github.overpas:treemap-chart:0.1.0")
82+
implementation("io.github.overpas:treemap-chart-compose:0.1.0")
8083
}
8184

8285
desktopMain.dependencies {
@@ -90,6 +93,9 @@ kotlin {
9093

9194
appleMain.dependencies {
9295
implementation(libs.ktor.client.darwin)
96+
97+
implementation("io.github.overpas:treemap-chart:0.1.0")
98+
implementation("io.github.overpas:treemap-chart-compose:0.1.0")
9399
}
94100
}
95101
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import androidx.compose.runtime.Stable
2+
import androidx.compose.ui.graphics.Color
3+
4+
@Stable
5+
sealed class ChartNode {
6+
7+
abstract val name: String
8+
9+
abstract val value: Double
10+
11+
abstract val percentage: Double
12+
13+
@Stable
14+
data class Leaf(
15+
override val name: String,
16+
override val value: Double,
17+
override val percentage: Double,
18+
val color: Color,
19+
) : ChartNode()
20+
21+
@Stable
22+
data class Section(
23+
override val name: String,
24+
override val value: Double,
25+
override val percentage: Double,
26+
val color: Color?,
27+
) : ChartNode()
28+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,185 @@
1+
import androidx.compose.foundation.background
2+
import androidx.compose.foundation.border
3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.material3.MaterialTheme
8+
import androidx.compose.material3.Text
19
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.LaunchedEffect
11+
import androidx.compose.runtime.derivedStateOf
12+
import androidx.compose.runtime.getValue
13+
import androidx.compose.runtime.mutableStateOf
14+
import androidx.compose.runtime.remember
15+
import androidx.compose.runtime.setValue
16+
import androidx.compose.ui.Alignment
17+
import androidx.compose.ui.Modifier
18+
import androidx.compose.ui.draw.drawWithContent
19+
import androidx.compose.ui.graphics.Color
20+
import androidx.compose.ui.text.TextStyle
21+
import androidx.compose.ui.text.style.TextAlign
22+
import androidx.compose.ui.unit.TextUnit
23+
import androidx.compose.ui.unit.dp
24+
import androidx.compose.ui.unit.isUnspecified
25+
import androidx.compose.ui.unit.sp
26+
import by.overpass.treemapchart.compose.TreemapChart
27+
import by.overpass.treemapchart.core.tree.Tree
28+
import by.overpass.treemapchart.core.tree.tree
229
import dev.johnoreilly.climatetrace.remote.CountryAssetEmissionsInfo
30+
import io.github.koalaplot.core.util.generateHueColorPalette
31+
import io.github.koalaplot.core.util.toString
32+
import kotlinx.coroutines.Dispatchers
33+
import kotlinx.coroutines.withContext
34+
335

436
@Composable
537
actual fun CountryAssetEmissionsInfoTreeMapChart(countryAssetEmissions: List<CountryAssetEmissionsInfo>) {
38+
var tree by remember { mutableStateOf<Tree<ChartNode>?>(null) }
39+
40+
LaunchedEffect(countryAssetEmissions) {
41+
tree = buildAssetTree(countryAssetEmissions ?: emptyList())
42+
}
43+
44+
tree?.let {
45+
TreemapChart(
46+
data = it,
47+
evaluateItem = ChartNode::value
48+
) { node, groupContent ->
49+
val export = node.data
50+
if (node.children.isEmpty() && export is ChartNode.Leaf) {
51+
LeafItem(item = export, onClick = { })
52+
} else if (export is ChartNode.Section) {
53+
SectionItem(export.color) {
54+
groupContent(node)
55+
}
56+
}
57+
}
58+
}
59+
}
60+
61+
62+
@Composable
63+
fun LeafItem(
64+
item: ChartNode.Leaf,
65+
modifier: Modifier = Modifier,
66+
onClick: (ChartNode.Leaf) -> Unit,
67+
) {
68+
Box(
69+
contentAlignment = Alignment.Center,
70+
modifier = modifier
71+
.border(0.5.dp, Color.White)
72+
.background(item.color)
73+
.clickable { onClick(item) }
74+
.padding(4.dp),
75+
) {
76+
ShrinkableHidableText(
77+
text = "${item.name}\n${item.percentage.toPercent(2)}",
78+
minSize = 6.sp,
79+
)
80+
}
81+
}
82+
83+
fun Double.toPercent(precision: Int): String = "${(this * 100.0f).toString(precision)}%"
84+
85+
86+
@Composable
87+
fun SectionItem(
88+
sectionColor: Color?,
89+
modifier: Modifier = Modifier,
90+
content: @Composable () -> Unit,
91+
) {
92+
if (sectionColor != null) {
93+
Box(
94+
modifier = modifier
95+
.background(sectionColor)
96+
) {
97+
content()
98+
}
99+
} else {
100+
content()
101+
}
102+
}
6103

104+
105+
106+
107+
suspend fun buildAssetTree(assetEmissionInfoList: List<CountryAssetEmissionsInfo>): Tree<ChartNode> = withContext(
108+
Dispatchers.Default) {
109+
val filteredList = assetEmissionInfoList
110+
.filter { it.emissions > 0 }
111+
.sortedByDescending(CountryAssetEmissionsInfo::emissions)
112+
.take(10)
113+
114+
val colors = generateHueColorPalette(filteredList.size)
115+
116+
val total = filteredList.sumOf { it.emissions.toDouble() } //.sumOf(CountryAssetEmissionsInfo::emissions)
117+
tree(
118+
ChartNode.Section(
119+
name = "Total",
120+
value = total,
121+
percentage = 1.0,
122+
color = null,
123+
),
124+
) {
125+
assetEmissionInfoList
126+
.filter { it.emissions > 0 }
127+
.sortedByDescending(CountryAssetEmissionsInfo::emissions)
128+
.take(10)
129+
.forEachIndexed { index, assetEmissionInfo ->
130+
val productPercentage = assetEmissionInfo.emissions / total
131+
node(
132+
ChartNode.Leaf(
133+
name = assetEmissionInfo.sector,
134+
value = assetEmissionInfo.emissions.toDouble(),
135+
percentage = productPercentage,
136+
color = colors[index]
137+
),
138+
)
139+
}
140+
141+
}
142+
}
143+
144+
145+
146+
@Suppress("LongParameterList")
147+
@Composable
148+
fun ShrinkableHidableText(
149+
text: String,
150+
minSize: TextUnit,
151+
modifier: Modifier = Modifier,
152+
shrinkSizeFactor: Float = 0.9F,
153+
textAlign: TextAlign = TextAlign.Center,
154+
style: TextStyle = MaterialTheme.typography.bodyLarge,
155+
) {
156+
var fontStyle by remember { mutableStateOf(style) }
157+
var shouldDraw by remember { mutableStateOf(false) }
158+
val show by remember { derivedStateOf { fontStyle.fontSize >= minSize } }
159+
if (show) {
160+
Text(
161+
text = text,
162+
modifier = modifier.drawWithContent {
163+
if (shouldDraw) {
164+
drawContent()
165+
}
166+
},
167+
textAlign = textAlign,
168+
onTextLayout = { result ->
169+
if (result.hasVisualOverflow) {
170+
fontStyle = fontStyle.copy(
171+
fontSize = fontStyle.fontSize * shrinkSizeFactor,
172+
letterSpacing = if (fontStyle.letterSpacing.isUnspecified) {
173+
fontStyle.letterSpacing
174+
} else {
175+
fontStyle.letterSpacing * shrinkSizeFactor
176+
},
177+
)
178+
} else {
179+
shouldDraw = true
180+
}
181+
},
182+
style = fontStyle,
183+
)
184+
}
7185
}

0 commit comments

Comments
 (0)