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
111import 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
231import 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
538actual 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}
0 commit comments