Skip to content

Commit 5db2fe4

Browse files
committed
feat(tokens): polish market cap graph
* Add shading while scrubbing * Add end cap dot while not scrubbing * Update generated data to support morphing between sets * Add tick vibration on scrub start * Display timestamp label while scrubbing Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 7047890 commit 5db2fe4

11 files changed

Lines changed: 663 additions & 301 deletions

File tree

apps/flipcash/features/tokens/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ dependencies {
6464
implementation(project(":libs:messaging"))
6565
implementation(project(":libs:logging"))
6666
implementation(project(":libs:quickresponse"))
67+
implementation(project(":libs:vibrator:bindings"))
6768
implementation(project(":ui:analytics"))
6869
implementation(project(":ui:core"))
6970
implementation(project(":ui:components"))

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/TokenInfoScreen.kt

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
2525
import com.flipcash.app.core.AppRoute
2626
import com.flipcash.app.core.money.RegionSelectionKind
2727
import com.flipcash.app.core.tokens.TokenSwapPurpose
28-
import com.flipcash.app.tokens.Period
2928
import com.flipcash.app.tokens.TokenInfoViewModel
29+
import com.flipcash.app.tokens.data.MarketTrend
3030
import com.flipcash.app.tokens.internal.components.info.MarketCapSection
31-
import com.flipcash.app.tokens.internal.components.info.MarketTrend
3231
import com.flipcash.app.tokens.internal.components.info.TokenBalance
3332
import com.flipcash.app.tokens.internal.components.info.TokenDetailsSection
34-
import com.flipcash.app.tokens.internal.components.info.generateMarketCapData
33+
import com.flipcash.app.tokens.internal.components.marketcap.generateMarketCapData
3534
import com.flipcash.features.tokens.R
3635
import com.getcode.theme.CodeTheme
3736
import com.getcode.ui.core.verticalScrollStateGradient
@@ -149,30 +148,21 @@ private fun TokenInfoScreen(
149148
// market cap
150149
state.marketCap?.let { mcap ->
151150
item {
152-
fun regenerateData(period: Period) {
153-
dispatch(
154-
TokenInfoViewModel.Event.OnHistoricalMarketCapDataUpdated(
155-
generateMarketCapData(
156-
period = period,
157-
trend = when (period) {
158-
Period.All -> MarketTrend.Bullish
159-
Period.Day -> MarketTrend.Bearish
160-
Period.Week -> MarketTrend.Sideways
161-
Period.Month -> MarketTrend.Volatile
162-
Period.Year -> MarketTrend.Bullish
163-
},
164-
currentMarketCap = mcap
165-
)
166-
)
167-
)
168-
}
169151
LaunchedEffect(state.historicalMarketCapData) {
170152
if (state.historicalMarketCapData.isNotEmpty()) {
171153
return@LaunchedEffect
172154
}
173155

174156
// generate sample data
175-
regenerateData(state.selectedPeriod)
157+
dispatch(
158+
TokenInfoViewModel.Event.OnHistoricalMarketCapDataUpdated(
159+
generateMarketCapData(
160+
mintDate = state.token!!.createdAt!!,
161+
currentMarketCap = mcap,
162+
period = state.selectedPeriod
163+
)
164+
)
165+
)
176166
}
177167

178168
MarketCapSection(
@@ -182,11 +172,19 @@ private fun TokenInfoScreen(
182172
marketCap = mcap,
183173
chartEnabled = state.marketCapChartEnabled,
184174
selectedPeriod = state.selectedPeriod,
185-
historicalData = state.historicalMarketCapData,
175+
rawHistoricalData = state.historicalMarketCapData,
186176
onPeriodSelected = {
187177
dispatch(TokenInfoViewModel.Event.OnMarketCapPeriodSelected(it))
188-
// also regenerate mcap data for sampling
189-
regenerateData(it)
178+
// generate sample data
179+
dispatch(
180+
TokenInfoViewModel.Event.OnHistoricalMarketCapDataUpdated(
181+
generateMarketCapData(
182+
mintDate = state.token!!.createdAt!!,
183+
currentMarketCap = mcap,
184+
period = it,
185+
)
186+
)
187+
)
190188
},
191189
)
192190
}
Lines changed: 135 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package com.flipcash.app.tokens.internal.components.info
22

3-
import androidx.compose.animation.AnimatedVisibility
43
import androidx.compose.animation.animateColorAsState
54
import androidx.compose.animation.core.animateFloatAsState
6-
import androidx.compose.animation.fadeIn
7-
import androidx.compose.animation.fadeOut
85
import androidx.compose.foundation.background
6+
import androidx.compose.foundation.layout.Box
97
import androidx.compose.foundation.layout.Column
108
import androidx.compose.foundation.layout.PaddingValues
119
import androidx.compose.foundation.layout.fillMaxWidth
@@ -23,32 +21,45 @@ import androidx.compose.ui.Modifier
2321
import androidx.compose.ui.draw.alpha
2422
import androidx.compose.ui.res.stringResource
2523
import androidx.compose.ui.unit.dp
26-
import com.flipcash.app.tokens.Period
24+
import com.flipcash.app.tokens.data.MarketCapPoint
25+
import com.flipcash.app.tokens.data.Period
26+
import com.flipcash.app.tokens.data.collapse
27+
import com.flipcash.app.tokens.internal.components.marketcap.MarketCapChart
2728
import com.flipcash.features.tokens.R
2829
import com.getcode.opencode.model.financial.Fiat
2930
import com.getcode.opencode.model.financial.minus
3031
import com.getcode.theme.CodeTheme
3132
import com.getcode.theme.extraSmall
32-
import com.getcode.ui.components.charts.ChartPoint
3333
import com.getcode.ui.components.charts.LineTrend
3434
import com.getcode.ui.components.charts.TrendType
3535
import com.getcode.ui.components.text.AnimatedNumberText
3636
import com.getcode.ui.utils.calculateEndPadding
37-
import com.getcode.ui.utils.calculateHorizontalPadding
3837
import com.getcode.ui.utils.calculateStartPadding
38+
import com.getcode.util.format
39+
import kotlinx.datetime.TimeZone
40+
import kotlinx.datetime.toLocalDateTime
41+
import kotlin.time.Clock
42+
import kotlin.time.Instant
3943

4044
@Composable
4145
internal fun MarketCapSection(
4246
marketCap: Fiat,
4347
chartEnabled: Boolean,
44-
historicalData: List<ChartPoint<Long, Long>>,
48+
rawHistoricalData: List<MarketCapPoint>,
4549
selectedPeriod: Period,
4650
modifier: Modifier = Modifier,
4751
contentPadding: PaddingValues = PaddingValues(),
4852
onPeriodSelected: (Period) -> Unit
4953
) {
5054
var highlightedCapPoint by remember {
51-
mutableStateOf<ChartPoint<Long, Long>?>(null)
55+
mutableStateOf<MarketCapPoint?>(null)
56+
}
57+
58+
val startCap by remember(selectedPeriod, rawHistoricalData) {
59+
derivedStateOf {
60+
val startQuarks = rawHistoricalData.collapse(selectedPeriod).firstOrNull()?.y ?: return@derivedStateOf null
61+
Fiat(startQuarks, marketCap.currencyCode)
62+
}
5263
}
5364

5465
val marketCapAtPoint by remember(marketCap, highlightedCapPoint) {
@@ -59,11 +70,10 @@ internal fun MarketCapSection(
5970
}
6071
}
6172

62-
val marketCapDiff by remember(marketCap, historicalData) {
73+
val marketCapDiff by remember(marketCap, startCap) {
6374
derivedStateOf {
64-
val startCapQuarks = historicalData.firstOrNull()?.y ?: return@derivedStateOf null
65-
val startCap = Fiat(startCapQuarks, marketCap.currencyCode)
66-
marketCap - startCap
75+
val start = startCap ?: return@derivedStateOf null
76+
marketCap - start
6777
}
6878
}
6979

@@ -86,47 +96,23 @@ internal fun MarketCapSection(
8696

8797
if (chartEnabled) {
8898
marketCapDiff?.let { change ->
89-
val isPositiveChange = change.decimalValue >= 0
90-
val changeColor by animateColorAsState(
91-
if (isPositiveChange) LineTrend.Up.color else LineTrend.Down.color
92-
)
93-
94-
val alpha by animateFloatAsState(
95-
if (highlightedCapPoint == null) 1f else 0f
96-
)
97-
98-
Text(
99-
modifier = Modifier
100-
.padding(start = contentPadding.calculateStartPadding())
101-
.padding(top = CodeTheme.dimens.grid.x2)
102-
.alpha(alpha)
103-
.background(
104-
color = changeColor.copy(0.20f),
105-
shape = MaterialTheme.shapes.extraSmall,
106-
).padding(
107-
vertical = 2.dp,
108-
horizontal = CodeTheme.dimens.grid.x1
109-
),
110-
text = change.formatted(
111-
extraPrefix = if (change.decimalValue >= 0) "+" else "-",
112-
suffix = when (selectedPeriod) {
113-
Period.All -> stringResource(R.string.label_marketCapAllTime)
114-
Period.Day -> stringResource(R.string.label_marketCapDay)
115-
Period.Week -> stringResource(R.string.label_marketCapWeek)
116-
Period.Month -> stringResource(R.string.label_marketCapMonth)
117-
Period.Year -> stringResource(R.string.label_marketCapYear)
118-
}
119-
),
120-
style = CodeTheme.typography.textSmall,
121-
color = changeColor,
122-
)
123-
124-
AnimatedVisibility(
125-
visible = highlightedCapPoint == null,
126-
enter = fadeIn(),
127-
exit = fadeOut()
99+
Box(modifier = Modifier
100+
.padding(
101+
start = contentPadding.calculateStartPadding(),
102+
top = CodeTheme.dimens.grid.x2,
103+
)
128104
) {
105+
MarketCapChangeLabel(
106+
change = change,
107+
isVisible = highlightedCapPoint == null,
108+
period = selectedPeriod,
109+
)
129110

111+
HighlightedPointLabel(
112+
point = highlightedCapPoint,
113+
isVisible = highlightedCapPoint != null,
114+
period = selectedPeriod,
115+
)
130116
}
131117
}
132118

@@ -136,14 +122,110 @@ internal fun MarketCapSection(
136122
.requiredHeight(240.dp),
137123
chartPadding = PaddingValues(end = contentPadding.calculateEndPadding()),
138124
periodPadding = PaddingValues(
139-
horizontal = contentPadding.calculateHorizontalPadding().times(0.75f)
125+
start = contentPadding.calculateStartPadding(),
126+
end = contentPadding.calculateEndPadding(),
140127
),
141-
data = historicalData,
128+
data = rawHistoricalData,
142129
trendType = TrendType.FirstVsLast,
143130
selectedPeriod = selectedPeriod,
144131
onPointHighlighted = { highlightedCapPoint = it },
145132
onPeriodSelected = onPeriodSelected,
146133
)
147134
}
148135
}
136+
}
137+
138+
@Composable
139+
private fun MarketCapChangeLabel(
140+
change: Fiat,
141+
isVisible: Boolean,
142+
period: Period,
143+
) {
144+
val isPositiveChange = change.decimalValue >= 0
145+
val changeColor by animateColorAsState(
146+
if (isPositiveChange) LineTrend.Up.color else LineTrend.Down.color
147+
)
148+
149+
val alpha by animateFloatAsState(
150+
if (isVisible) 1f else 0f
151+
)
152+
153+
Text(
154+
modifier = Modifier
155+
.alpha(alpha)
156+
.background(
157+
color = changeColor.copy(0.20f),
158+
shape = MaterialTheme.shapes.extraSmall,
159+
).padding(
160+
vertical = 2.dp,
161+
horizontal = CodeTheme.dimens.grid.x1
162+
),
163+
text = change.formatted(
164+
extraPrefix = if (change.decimalValue >= 0) "+" else "-",
165+
suffix = when (period) {
166+
Period.All -> stringResource(R.string.label_marketCapAllTime)
167+
Period.Day -> stringResource(R.string.label_marketCapDay)
168+
Period.Week -> stringResource(R.string.label_marketCapWeek)
169+
Period.Month -> stringResource(R.string.label_marketCapMonth)
170+
Period.Year -> stringResource(R.string.label_marketCapYear)
171+
}
172+
),
173+
style = CodeTheme.typography.textSmall,
174+
color = changeColor,
175+
)
176+
}
177+
178+
@Composable
179+
private fun HighlightedPointLabel(
180+
point: MarketCapPoint?,
181+
period: Period,
182+
isVisible: Boolean,
183+
) {
184+
val alpha by animateFloatAsState(
185+
if (isVisible) 1f else 0f
186+
)
187+
188+
val timeLabel = remember(point, period) {
189+
val epoch = point?.x ?: return@remember null
190+
val instant = Instant.fromEpochMilliseconds(epoch)
191+
val now = Clock.System.now()
192+
val isCurrentYear = instant.toLocalDateTime(TimeZone.currentSystemDefault()).year ==
193+
now.toLocalDateTime(TimeZone.currentSystemDefault()).year
194+
195+
when (period) {
196+
Period.All -> instant.format("MMMM d, yyyy")
197+
Period.Day -> instant.format("hh:mm a")
198+
Period.Week -> {
199+
if (isCurrentYear) {
200+
instant.format("MMMM d")
201+
} else {
202+
instant.format("MMMM d, yyyy")
203+
}
204+
}
205+
Period.Month -> {
206+
if (isCurrentYear) {
207+
instant.format("MMMM d")
208+
} else {
209+
instant.format("MMMM d, yyyy")
210+
}
211+
}
212+
Period.Year -> {
213+
if (isCurrentYear) {
214+
instant.format("MMMM d")
215+
} else {
216+
instant.format("MMMM d, yyyy")
217+
}
218+
}
219+
}
220+
}
221+
222+
if (timeLabel != null) {
223+
Text(
224+
modifier = Modifier
225+
.alpha(alpha),
226+
text = timeLabel,
227+
style = CodeTheme.typography.textSmall,
228+
color = CodeTheme.colors.textSecondary,
229+
)
230+
}
149231
}

0 commit comments

Comments
 (0)