Skip to content

Commit b7dc20f

Browse files
committed
add treemap actual for andorid and make country list collapsible
1 parent 473a3fb commit b7dc20f

File tree

9 files changed

+371
-35
lines changed

9 files changed

+371
-35
lines changed
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: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,187 @@
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.fillMaxWidth
7+
import androidx.compose.foundation.layout.height
8+
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.material3.MaterialTheme
10+
import androidx.compose.material3.Text
111
import androidx.compose.runtime.Composable
12+
import androidx.compose.runtime.LaunchedEffect
13+
import androidx.compose.runtime.derivedStateOf
14+
import androidx.compose.runtime.getValue
15+
import androidx.compose.runtime.mutableStateOf
16+
import androidx.compose.runtime.remember
17+
import androidx.compose.runtime.setValue
18+
import androidx.compose.ui.Alignment
19+
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.draw.drawWithContent
21+
import androidx.compose.ui.graphics.Color
22+
import androidx.compose.ui.text.TextStyle
23+
import androidx.compose.ui.text.style.TextAlign
24+
import androidx.compose.ui.unit.TextUnit
25+
import androidx.compose.ui.unit.dp
26+
import androidx.compose.ui.unit.isUnspecified
27+
import androidx.compose.ui.unit.sp
28+
import by.overpass.treemapchart.compose.TreemapChart
29+
import by.overpass.treemapchart.core.tree.Tree
30+
import by.overpass.treemapchart.core.tree.tree
231
import dev.johnoreilly.climatetrace.remote.CountryAssetEmissionsInfo
32+
import io.github.koalaplot.core.util.generateHueColorPalette
33+
import io.github.koalaplot.core.util.toString
34+
import kotlinx.coroutines.Dispatchers
35+
import kotlinx.coroutines.withContext
336

437
@Composable
538
actual fun CountryAssetEmissionsInfoTreeMapChart(countryAssetEmissions: List<CountryAssetEmissionsInfo>) {
39+
var tree by remember { mutableStateOf<Tree<ChartNode>?>(null) }
640

41+
LaunchedEffect(countryAssetEmissions) {
42+
tree = buildAssetTree(countryAssetEmissions ?: emptyList())
43+
}
44+
45+
Column(Modifier.height(500.dp).fillMaxWidth(0.8f)) {
46+
tree?.let {
47+
TreemapChart(
48+
data = it,
49+
evaluateItem = ChartNode::value
50+
) { node, groupContent ->
51+
val export = node.data
52+
if (node.children.isEmpty() && export is ChartNode.Leaf) {
53+
LeafItem(item = export, onClick = { })
54+
} else if (export is ChartNode.Section) {
55+
SectionItem(export.color) {
56+
groupContent(node)
57+
}
58+
}
59+
}
60+
}
61+
}
62+
}
63+
64+
65+
66+
@Composable
67+
fun LeafItem(
68+
item: ChartNode.Leaf,
69+
modifier: Modifier = Modifier,
70+
onClick: (ChartNode.Leaf) -> Unit,
71+
) {
72+
Box(
73+
contentAlignment = Alignment.Center,
74+
modifier = modifier
75+
.border(0.5.dp, Color.White)
76+
.background(item.color)
77+
.clickable { onClick(item) }
78+
.padding(4.dp),
79+
) {
80+
ShrinkableHidableText(
81+
text = "${item.name}\n${item.percentage.toPercent(2)}",
82+
minSize = 6.sp,
83+
)
84+
}
85+
}
86+
87+
fun Double.toPercent(precision: Int): String = "${(this * 100.0f).toString(precision)}%"
88+
89+
90+
@Composable
91+
fun SectionItem(
92+
sectionColor: Color?,
93+
modifier: Modifier = Modifier,
94+
content: @Composable () -> Unit,
95+
) {
96+
if (sectionColor != null) {
97+
Box(
98+
modifier = modifier
99+
.background(sectionColor)
100+
) {
101+
content()
102+
}
103+
} else {
104+
content()
105+
}
106+
}
107+
108+
109+
suspend fun buildAssetTree(assetEmissionInfoList: List<CountryAssetEmissionsInfo>): Tree<ChartNode> = withContext(
110+
Dispatchers.Default) {
111+
val filteredList = assetEmissionInfoList
112+
.filter { it.emissions > 0 }
113+
.sortedByDescending(CountryAssetEmissionsInfo::emissions)
114+
.take(10)
115+
116+
val colors = generateHueColorPalette(filteredList.size)
117+
118+
val total = filteredList.sumOf { it.emissions.toDouble() } //.sumOf(CountryAssetEmissionsInfo::emissions)
119+
tree(
120+
ChartNode.Section(
121+
name = "Total",
122+
value = total,
123+
percentage = 1.0,
124+
color = null,
125+
),
126+
) {
127+
assetEmissionInfoList
128+
.filter { it.emissions > 0 }
129+
.sortedByDescending(CountryAssetEmissionsInfo::emissions)
130+
.take(10)
131+
.forEachIndexed { index, assetEmissionInfo ->
132+
val productPercentage = assetEmissionInfo.emissions / total
133+
node(
134+
ChartNode.Leaf(
135+
name = assetEmissionInfo.sector,
136+
value = assetEmissionInfo.emissions.toDouble(),
137+
percentage = productPercentage,
138+
color = colors[index]
139+
),
140+
)
141+
}
142+
143+
}
144+
}
145+
146+
147+
148+
@Suppress("LongParameterList")
149+
@Composable
150+
fun ShrinkableHidableText(
151+
text: String,
152+
minSize: TextUnit,
153+
modifier: Modifier = Modifier,
154+
shrinkSizeFactor: Float = 0.9F,
155+
textAlign: TextAlign = TextAlign.Center,
156+
style: TextStyle =MaterialTheme.typography.bodyMedium
157+
) {
158+
var fontStyle by remember { mutableStateOf(style) }
159+
var shouldDraw by remember { mutableStateOf(false) }
160+
val show by remember { derivedStateOf { fontStyle.fontSize >= minSize } }
161+
if (show) {
162+
Text(
163+
text = text,
164+
modifier = modifier.drawWithContent {
165+
if (shouldDraw) {
166+
drawContent()
167+
}
168+
},
169+
textAlign = textAlign,
170+
onTextLayout = { result ->
171+
if (result.hasVisualOverflow) {
172+
fontStyle = fontStyle.copy(
173+
fontSize = fontStyle.fontSize * shrinkSizeFactor,
174+
letterSpacing = if (fontStyle.letterSpacing.isUnspecified) {
175+
fontStyle.letterSpacing
176+
} else {
177+
fontStyle.letterSpacing * shrinkSizeFactor
178+
},
179+
)
180+
} else {
181+
shouldDraw = true
182+
}
183+
},
184+
style = fontStyle,
185+
)
186+
}
7187
}

composeApp/src/commonMain/kotlin/dev/johnoreilly/climatetrace/ui/ClimateTraceScreen.kt

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package dev.johnoreilly.climatetrace.ui
22

33
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.animation.core.Spring
5+
import androidx.compose.animation.core.SpringSpec
6+
import androidx.compose.animation.core.animateDpAsState
47
import androidx.compose.animation.fadeIn
58
import androidx.compose.animation.fadeOut
69
import androidx.compose.foundation.background
@@ -46,6 +49,8 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
4649
import androidx.compose.ui.unit.dp
4750
import dev.johnoreilly.climatetrace.remote.ClimateTraceApi
4851
import dev.johnoreilly.climatetrace.remote.Country
52+
import dev.johnoreilly.climatetrace.ui.utils.PanelState
53+
import dev.johnoreilly.climatetrace.ui.utils.ResizablePanel
4954
import dev.johnoreilly.climatetrace.viewmodel.ClimateTraceViewModel
5055
import org.koin.compose.koinInject
5156
import org.koin.core.component.inject
@@ -66,6 +71,18 @@ fun ClimateTraceScreen() {
6671
val isLoadingCountryDetails by viewModel.isLoadingCountryDetails.collectAsState()
6772

6873

74+
val panelState = remember { PanelState() }
75+
76+
val animatedSize = if (panelState.splitter.isResizing) {
77+
if (panelState.isExpanded) panelState.expandedSize else panelState.collapsedSize
78+
} else {
79+
animateDpAsState(
80+
if (panelState.isExpanded) panelState.expandedSize else panelState.collapsedSize,
81+
SpringSpec(stiffness = Spring.StiffnessLow)
82+
).value
83+
}
84+
85+
6986
Row(Modifier.fillMaxSize()) {
7087

7188
if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) {
@@ -90,14 +107,22 @@ fun ClimateTraceScreen() {
90107
)
91108
}
92109
} else {
93-
Box(Modifier.width(250.dp).fillMaxHeight().background(color = Color.LightGray)) {
94-
CountryListView(
95-
countryList = countryList,
96-
selectedCountry = selectedCountry,
97-
isLoading = isLoadingCountries
98-
) { country ->
99-
viewModel.fetchCountryDetails(country)
100-
}
110+
111+
ResizablePanel(
112+
Modifier.width(animatedSize).fillMaxHeight(),
113+
title = "Countries",
114+
state = panelState
115+
) {
116+
117+
//Box(Modifier.width(250.dp).fillMaxHeight().background(color = Color.LightGray)) {
118+
CountryListView(
119+
countryList = countryList,
120+
selectedCountry = selectedCountry,
121+
isLoading = isLoadingCountries
122+
) { country ->
123+
viewModel.fetchCountryDetails(country)
124+
}
125+
//}
101126
}
102127

103128
Spacer(modifier = Modifier.width(1.dp).fillMaxHeight())

composeApp/src/commonMain/kotlin/dev/johnoreilly/climatetrace/ui/CountryInfoDetailedView.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
99
import androidx.compose.foundation.layout.height
1010
import androidx.compose.foundation.layout.padding
1111
import androidx.compose.foundation.layout.size
12-
import androidx.compose.foundation.layout.width
1312
import androidx.compose.foundation.layout.wrapContentSize
1413
import androidx.compose.foundation.rememberScrollState
1514
import androidx.compose.foundation.verticalScroll
@@ -86,9 +85,7 @@ fun CountryInfoDetailedView(
8685

8786
Spacer(modifier = Modifier.size(16.dp))
8887

89-
Column(Modifier.height(500.dp).fillMaxWidth(0.8f)) {
90-
CountryAssetEmissionsInfoTreeMapChart(countryAssetEmissionsList)
91-
}
88+
CountryAssetEmissionsInfoTreeMapChart(countryAssetEmissionsList)
9289
Spacer(modifier = Modifier.size(16.dp))
9390

9491
SectorEmissionsPieChart(countryAssetEmissionsList)

0 commit comments

Comments
 (0)