Updated on 2026-08-14
This commit is contained in:
parent
aac5e5a4a9
commit
5e083c40d9
6 changed files with 88 additions and 51 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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%",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue