Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-01 15:39:20 +02:00
parent aac5e5a4a9
commit 5e083c40d9
6 changed files with 88 additions and 51 deletions

View file

@ -38,11 +38,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.foryou.impl.R
import com.tangem.features.foryou.impl.components.state.DonutSegmentUM
import java.math.BigDecimal
import kotlin.math.atan2
import kotlin.math.hypot
import kotlin.math.min
@ -164,7 +166,7 @@ internal fun DonutChart(
}
// Precompute each slice's [start, sweep] once.
val sweeps = segments.map { it.weight.coerceIn(0f, 1f) * 360f }
val sweeps = segments.map { it.weight.toFloat().coerceIn(0f, 1f) * 360f }
val starts = sweeps.runningFold(startAngle) { acc, sweep -> acc + sweep }
// 2. Slices — reversed so slice 0 sits on top of its neighbor. Each slice gets its own
@ -247,7 +249,7 @@ private fun segmentIndexAt(
// Degrees clockwise from 3 o'clock — same convention as Canvas.drawArc.
val angle = Math.toDegrees(atan2(dy, dx).toDouble()).toFloat().mod(360f)
val sweeps = segments.map { it.weight.coerceIn(0f, 1f) * 360f }
val sweeps = segments.map { it.weight.toFloat().coerceIn(0f, 1f) * 360f }
val starts = sweeps.runningFold(startAngle) { acc, sweep -> acc + sweep }
for (i in segments.indices) {
if (sweeps[i] <= 0f) continue
@ -351,10 +353,30 @@ private fun PreviewDonutChart() {
selectedIndex = selectedIndex,
onSegmentClick = { index -> selectedIndex = index.takeIf { it != selectedIndex } },
segments = listOf(
DonutSegmentUM(weight = 0.55f, color = TangemTheme.colors3.border.brand),
DonutSegmentUM(weight = 0.07f, color = TangemTheme.colors3.border.accent.violet),
DonutSegmentUM(weight = 0.06f, color = TangemTheme.colors3.border.accent.red),
DonutSegmentUM(weight = 0.05f, color = TangemTheme.colors3.border.accent.green),
DonutSegmentUM(
weight = BigDecimal(0.55),
color = TangemTheme.colors3.border.brand,
title = stringReference("Ethereum"),
fiatValue = stringReference("$5,720.22"),
),
DonutSegmentUM(
weight = BigDecimal(0.07),
color = TangemTheme.colors3.border.accent.violet,
title = stringReference("Solana"),
fiatValue = stringReference("$728.30"),
),
DonutSegmentUM(
weight = BigDecimal(0.06),
color = TangemTheme.colors3.border.accent.red,
title = stringReference("Polkadot"),
fiatValue = stringReference("$624.26"),
),
DonutSegmentUM(
weight = BigDecimal(0.05),
color = TangemTheme.colors3.border.accent.green,
title = stringReference("Tether"),
fiatValue = stringReference("$520.18"),
),
),
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {

View file

@ -27,6 +27,9 @@ import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import com.tangem.core.ui.ds2.surface.TangemSurface
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@ -64,8 +67,8 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun DonutSegmentTooltip(
expanded: Boolean,
title: String,
fiatValue: String,
title: TextReference,
fiatValue: TextReference,
percent: String,
positionProvider: PopupPositionProvider,
onDismissRequest: () -> Unit,
@ -100,8 +103,8 @@ private const val DISMISSED_SCALE = 0.8f
@Composable
private fun TooltipPill(
expandedStates: MutableTransitionState<Boolean>,
title: String,
fiatValue: String,
title: TextReference,
fiatValue: TextReference,
percent: String,
modifier: Modifier = Modifier,
) {
@ -145,7 +148,7 @@ private fun TooltipPill(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = title,
text = title.resolveReference(),
color = TangemTheme.colors3.text.primary,
style = TangemTheme.typography3.caption.medium,
maxLines = 1,
@ -153,13 +156,13 @@ private fun TooltipPill(
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = fiatValue,
text = fiatValue.resolveReference(),
color = TangemTheme.colors3.text.primary,
style = TangemTheme.typography3.caption.medium,
maxLines = 1,
)
Text(
text = "$percent%",
text = "$percent",
color = TangemTheme.colors3.text.tertiary,
style = TangemTheme.typography3.caption.medium,
maxLines = 1,
@ -185,9 +188,9 @@ private fun PreviewDonutSegmentTooltip() {
) {
TooltipPill(
expandedStates = remember { MutableTransitionState(true) },
title = "Ethereum",
fiatValue = "$5,720.22",
percent = "57.5",
title = stringReference("Ethereum"),
fiatValue = stringReference("$5,720.22"),
percent = "57.5%",
)
}
}

View file

@ -51,9 +51,14 @@ import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.button.SecondaryTangemButton
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds2.surface.TangemSurface
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.foryou.impl.R
@ -63,6 +68,7 @@ import com.tangem.features.foryou.impl.components.state.DonutSegmentUM
import com.tangem.features.foryou.impl.components.state.MarketChartUM
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.math.BigDecimal
@Composable
internal fun MarketChart(marketChart: MarketChartUM, modifier: Modifier = Modifier) {
@ -200,7 +206,7 @@ private fun DonutSegmentTooltipBlock(
if (selectedIndex != null) shownIndex = selectedIndex
val isExpanded = selectedIndex?.let(segments::getOrNull) != null
val shownSegment = shownIndex?.let(segments::getOrNull)
val shownSegment = shownIndex?.let(segments::getOrNull) ?: return
val positionProvider = remember(
shownIndex,
segments,
@ -225,24 +231,18 @@ private fun DonutSegmentTooltipBlock(
DonutSegmentTooltip(
expanded = isExpanded,
positionProvider = positionProvider,
title = shownSegment?.title.orEmpty(),
fiatValue = shownSegment?.fiatValue.orEmpty(),
percent = shownSegment?.let { formatSegmentPercent(it.weight) }.orEmpty(),
title = shownSegment.title,
fiatValue = shownSegment.fiatValue,
percent = shownSegment.weight.format { percent() },
onDismissRequest = onDismissRequest,
)
}
@Suppress("MagicNumber")
private fun formatSegmentPercent(weight: Float): String {
val percent = weight.coerceIn(0f, 1f) * 100
return if (percent % 1f == 0f) "${percent.toInt()}" else "%.2f".format(percent)
}
private val DonutStrokeWidth = 28.dp
private val DonutStartAngle = -90f
@Composable
private fun ColumnScope.TopHoldingBlock(assetCount: Int, topHoldingPercent: Float) {
private fun ColumnScope.TopHoldingBlock(assetCount: Int, topHoldingPercent: TextReference) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = pluralStringResourceSafe(R.plurals.market_chart_assets_android, assetCount, assetCount),
@ -252,7 +252,7 @@ private fun ColumnScope.TopHoldingBlock(assetCount: Int, topHoldingPercent: Floa
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = stringResourceSafe(R.string.market_chart_top_holding, formatSegmentPercent(topHoldingPercent)),
text = topHoldingPercent.resolveReference(),
color = TangemTheme.colors3.text.primary,
style = TangemTheme.typography3.heading.small,
)
@ -361,7 +361,7 @@ private fun MarketChart_Preview(
@Composable
private fun previewMarketChartState(scenario: MarketChartPreviewScenario): MarketChartUM = when (scenario) {
MarketChartPreviewScenario.DISPLAYED -> MarketChartUM.Loaded(
topHoldingPercent = 0.41f,
topHoldingPercent = stringReference("Top holding 41%"),
aiInsight = AiInsightUM.Displayed(
"Your portfolio leans on a single asset BTC is 42% of holdings. Stablecoins add 23% " +
"buffer. Consider trimming concentration for a smoother ride",
@ -369,18 +369,28 @@ private fun previewMarketChartState(scenario: MarketChartPreviewScenario): Marke
donutChart = previewLoadedDonut(),
)
MarketChartPreviewScenario.ASK_AI -> MarketChartUM.Loaded(
topHoldingPercent = 0.41f,
topHoldingPercent = stringReference("Top holding 41%"),
aiInsight = AiInsightUM.AskAiInsight(askAiInsightClick = {}),
donutChart = previewLoadedDonut(),
)
MarketChartPreviewScenario.NO_AI -> MarketChartUM.Loaded(
topHoldingPercent = 0.41f,
topHoldingPercent = stringReference("Top holding 41%"),
aiInsight = AiInsightUM.Hide,
donutChart = DonutChartUM.Loaded(
totalAmount = "$10,12345678912.1333",
donutSegmentList = listOf(
DonutSegmentUM(weight = 0.55f, color = TangemTheme.colors3.border.brand),
DonutSegmentUM(weight = 0.45f, color = TangemTheme.colors3.border.accent.green),
DonutSegmentUM(
weight = BigDecimal(0.55),
color = TangemTheme.colors3.border.brand,
title = stringReference("Ethereum"),
fiatValue = stringReference("$5,720.22"),
),
DonutSegmentUM(
weight = BigDecimal(0.45),
color = TangemTheme.colors3.border.accent.green,
title = stringReference("Solana"),
fiatValue = stringReference("$728.30"),
),
),
),
)
@ -393,28 +403,28 @@ private fun previewLoadedDonut(): DonutChartUM.Loaded = DonutChartUM.Loaded(
totalAmount = "$10,123456.1333",
donutSegmentList = listOf(
DonutSegmentUM(
weight = 0.55f,
weight = BigDecimal(0.55),
color = TangemTheme.colors3.border.brand,
title = "Ethereum",
fiatValue = "$5,720.22",
title = stringReference("Ethereum"),
fiatValue = stringReference("$5,720.22"),
),
DonutSegmentUM(
weight = 0.077f,
weight = BigDecimal(0.077),
color = TangemTheme.colors3.border.accent.violet,
title = "Solana",
fiatValue = "$728.30",
title = stringReference("Solana"),
fiatValue = stringReference("$728.30"),
),
DonutSegmentUM(
weight = 0.0666f,
weight = BigDecimal(0.0666),
color = TangemTheme.colors3.border.accent.red,
title = "Polkadot",
fiatValue = "$624.26",
title = stringReference("Polkadot"),
fiatValue = stringReference("$624.26"),
),
DonutSegmentUM(
weight = 0.05f,
weight = BigDecimal(0.05),
color = TangemTheme.colors3.border.accent.green,
title = "Tether",
fiatValue = "$520.18",
title = stringReference("Tether"),
fiatValue = stringReference("$520.18"),
),
),
)

View file

@ -42,7 +42,7 @@ internal fun segmentTooltipPositionProvider(
val centerY = chartSize.height / 2f
val innerRadius = diameter / 2f - strokePx / 2
// End angle of the selected slice (before its round cap) — same layout as DonutChart's drawing pass.
val sweeps = segments.map { it.weight.coerceIn(0f, 1f) * 360f }
val sweeps = segments.map { it.weight.toFloat().coerceIn(0f, 1f) * 360f }
val endAngleDeg = startAngle + sweeps.take(selectedIndex + 1).sum()
val endAngleRad = Math.toRadians(endAngleDeg.toDouble())
val anchorLocal = Offset(

View file

@ -1,6 +1,8 @@
package com.tangem.features.foryou.impl.components.state
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.extensions.TextReference
import java.math.BigDecimal
/**
* One colored slice of a [com.tangem.features.foryou.impl.components.DonutChart].
@ -16,8 +18,8 @@ import androidx.compose.ui.graphics.Color
* tooltip next to the share. Empty by default.
*/
internal data class DonutSegmentUM(
val weight: Float,
val color: Color,
val title: String = "",
val fiatValue: String = "",
val weight: BigDecimal,
val title: TextReference,
val fiatValue: TextReference,
)

View file

@ -1,6 +1,7 @@
package com.tangem.features.foryou.impl.components.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed class MarketChartUM(
@ -10,8 +11,7 @@ internal sealed class MarketChartUM(
data class Loaded(
override val donutChart: DonutChartUM.Loaded,
override val aiInsight: AiInsightUM = AiInsightUM.Hide,
/* from 0 to 1 */
val topHoldingPercent: Float,
val topHoldingPercent: TextReference,
) : MarketChartUM(
donutChart = donutChart,
aiInsight = aiInsight,