Skip to content

Commit

Permalink
Show the selected entry from the vault chart (#249)
Browse files Browse the repository at this point in the history
User can select data point from the chart to view historical details.


[untitled.webm](https://github.com/user-attachments/assets/1a6733b4-d135-4cae-af41-fc416c3687e4)
  • Loading branch information
ruixhuang authored Oct 16, 2024
1 parent 0559e08 commit 9e5d11b
Show file tree
Hide file tree
Showing 6 changed files with 327 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package exchange.dydx.trading.feature.vault.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import exchange.dydx.abacus.protocols.LocalizerProtocol
import exchange.dydx.platformui.compose.collectAsStateWithLifecycle
import exchange.dydx.platformui.designSystem.theme.ThemeColor
import exchange.dydx.platformui.designSystem.theme.ThemeFont
import exchange.dydx.platformui.designSystem.theme.ThemeShapes
import exchange.dydx.platformui.designSystem.theme.dydxDefault
import exchange.dydx.platformui.designSystem.theme.themeColor
import exchange.dydx.platformui.designSystem.theme.themeFont
import exchange.dydx.platformui.theme.DydxThemedPreviewSurface
import exchange.dydx.platformui.theme.MockLocalizer
import exchange.dydx.trading.common.component.DydxComponent
import exchange.dydx.trading.feature.shared.views.SignedAmountView

@Preview
@Composable
fun Preview_DydxVaultChartSelectedInfoView() {
DydxThemedPreviewSurface {
DydxVaultChartSelectedInfoView.Content(Modifier, DydxVaultChartSelectedInfoView.ViewState.preview)
}
}

object DydxVaultChartSelectedInfoView : DydxComponent {
data class ViewState(
val localizer: LocalizerProtocol,
val change: SignedAmountView.ViewState? = null,
val currentValue: String? = null,
val entryDate: String? = null,
) {
companion object {
val preview = ViewState(
localizer = MockLocalizer(),
change = SignedAmountView.ViewState.preview,
currentValue = "$1.0M",
entryDate = "2021-01-01",
)
}
}

@Composable
override fun Content(modifier: Modifier) {
val viewModel: DydxVaultChartSelectedInfoViewModel = hiltViewModel()

val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value
Content(modifier, state)
}

@Composable
fun Content(modifier: Modifier, state: ViewState?) {
if (state == null) {
return
}

Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(ThemeShapes.VerticalPadding),
) {
Text(
text = state.entryDate ?: "",
modifier = Modifier,
style = TextStyle.dydxDefault
.themeFont(fontSize = ThemeFont.FontSize.base)
.themeColor(ThemeColor.SemanticColor.text_tertiary),
)

Row(
modifier = Modifier,
horizontalArrangement = Arrangement.spacedBy(ThemeShapes.HorizontalPadding),
) {
Text(
text = state.currentValue ?: "-",
modifier = modifier,
style = TextStyle.dydxDefault
.themeFont(fontSize = ThemeFont.FontSize.medium)
.themeColor(ThemeColor.SemanticColor.text_primary),
)

SignedAmountView.Content(
modifier = modifier,
state = state.change,
textStyle = TextStyle.dydxDefault
.themeFont(fontSize = ThemeFont.FontSize.medium),
)

Spacer(modifier = Modifier.weight(1f))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package exchange.dydx.trading.feature.vault.components

import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import exchange.dydx.abacus.functional.vault.VaultHistoryEntry
import exchange.dydx.abacus.protocols.LocalizerProtocol
import exchange.dydx.trading.common.DydxViewModel
import exchange.dydx.trading.common.formatter.DydxFormatter
import exchange.dydx.trading.feature.shared.views.SignedAmountView
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import javax.inject.Inject

@HiltViewModel
class DydxVaultChartSelectedInfoViewModel @Inject constructor(
private val localizer: LocalizerProtocol,
private val formatter: DydxFormatter,
private val selectedChartEntryFlow: Flow<VaultHistoryEntry?>,
private val vaultHistoryFlow: Flow<@JvmSuppressWildcards List<VaultHistoryEntry>?>,
private val chartTypeFlow: Flow<@JvmSuppressWildcards ChartType?>,
) : ViewModel(), DydxViewModel {

val state: Flow<DydxVaultChartSelectedInfoView.ViewState?> =
combine(
vaultHistoryFlow,
chartTypeFlow.filterNotNull(),
selectedChartEntryFlow,
) { history, chartType, selectedChartEntry ->
createViewState(history, chartType, selectedChartEntry)
}
.distinctUntilChanged()

private fun createViewState(
history: List<VaultHistoryEntry>?,
chartType: ChartType,
selectedChartEntry: VaultHistoryEntry?
): DydxVaultChartSelectedInfoView.ViewState {
val (value, percent) = when (chartType) {
ChartType.EQUITY -> createDiffs(
history?.firstOrNull()?.equity,
selectedChartEntry?.equity,
)

ChartType.PNL -> createDiffs(
history?.firstOrNull()?.totalPnl,
selectedChartEntry?.totalPnl,
)
}
val curValue = when (chartType) {
ChartType.EQUITY -> selectedChartEntry?.equity
ChartType.PNL -> selectedChartEntry?.totalPnl
}
return DydxVaultChartSelectedInfoView.ViewState(
localizer = localizer,
currentValue = formatter.dollar(curValue, digits = 2),
entryDate = formatter.dateTime(selectedChartEntry?.dateInstance),
change = SignedAmountView.ViewState.fromDouble(value) {
if (it == 0.0) {
""
} else {
val dollarValue = formatter.dollar(value, digits = 2) ?: "-"
val percentValue = formatter.percent(percent, 2) ?: "-"
"$dollarValue ($percentValue)"
}
},
)
}

private fun createDiffs(first: Double?, current: Double?): Pair<Double, Double> {
if (first == null || current == null) {
return Pair(0.0, 0.0)
}
val percent = if (first != 0.0) {
(current - first) / first
} else {
0.0
}
return Pair(current - first, percent)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package exchange.dydx.trading.feature.vault.components

import android.util.Half.toFloat
import androidx.lifecycle.ViewModel
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.highlight.Highlight
import com.github.mikephil.charting.listener.OnChartValueSelectedListener
import dagger.hilt.android.lifecycle.HiltViewModel
import exchange.dydx.abacus.functional.vault.VaultHistoryEntry
import exchange.dydx.abacus.protocols.LocalizerProtocol
Expand All @@ -17,15 +18,18 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import java.time.Duration
import java.time.Instant
import javax.inject.Inject

@HiltViewModel
class DydxVaultChartViewModel @Inject constructor(
private val localizer: LocalizerProtocol,
private val abacusStateManager: AbacusStateManagerProtocol,
) : ViewModel(), DydxViewModel {
private val selectedChartEntry: MutableStateFlow<VaultHistoryEntry?>,
private val vaultHistory: MutableStateFlow<List<VaultHistoryEntry>?>,
private val chartType: MutableStateFlow<ChartType?>,
) : ViewModel(), DydxViewModel, OnChartValueSelectedListener {

private val typeIndex = MutableStateFlow(0)
private val resolutionIndex = MutableStateFlow(1)
Expand All @@ -42,6 +46,10 @@ class DydxVaultChartViewModel @Inject constructor(
}
.distinctUntilChanged()

init {
chartType.value = ChartType.allTypes[typeIndex.value]
}

private fun createViewState(
history: IList<VaultHistoryEntry>?,
currentTypeIndex: Int,
Expand All @@ -53,6 +61,7 @@ class DydxVaultChartViewModel @Inject constructor(
typeIndex = currentTypeIndex,
onTypeChanged = {
typeIndex.value = it
chartType.value = ChartType.allTypes[it]
},
resolutionTitles = ChartResolution.allResolutions.map { it.title(localizer) },
resolutionIndex = currentResolutionIndex,
Expand All @@ -66,6 +75,7 @@ class DydxVaultChartViewModel @Inject constructor(
resolution = ChartResolution.allResolutions[currentResolutionIndex],
),
lineWidth = 3.0,
selectionListener = this,
),
)
}
Expand All @@ -78,13 +88,14 @@ class DydxVaultChartViewModel @Inject constructor(
val filtered = history?.filter { entry ->
val now = Clock.System.now()
val then = entry.dateInstance ?: return@filter false
val diff = now.toEpochMilliseconds() - then.toEpochMilliseconds()
val diff = now.toEpochMilliseconds() - then.toEpochMilli()
when (resolution) {
ChartResolution.DAY -> diff <= Duration.ofDays(1).toMillis()
ChartResolution.WEEK -> diff <= Duration.ofDays(7).toMillis()
ChartResolution.MONTH -> diff <= Duration.ofDays(30).toMillis()
}
}?.reversed()
vaultHistory.value = filtered
if (filtered.isNullOrEmpty()) {
return LineChartDataSet(emptyList(), type.title(localizer))
}
Expand All @@ -98,16 +109,24 @@ class DydxVaultChartViewModel @Inject constructor(
if (x == null || y == null) {
return@map null
}
Entry(x, y)
Entry(x, y, entry)
}
return LineChartDataSet(entries, type.title(localizer))
}

override fun onValueSelected(e: Entry?, h: Highlight?) {
selectedChartEntry.value = e?.data as? VaultHistoryEntry
}

override fun onNothingSelected() {
selectedChartEntry.value = null
}
}

private val VaultHistoryEntry.dateInstance: Instant?
get() = date?.let { Instant.fromEpochMilliseconds(it.toLong()) }
internal val VaultHistoryEntry.dateInstance: Instant?
get() = date?.let { Instant.ofEpochMilli(it.toLong()) }

private enum class ChartType {
enum class ChartType {
PNL,
EQUITY;

Expand Down
Loading

0 comments on commit 9e5d11b

Please sign in to comment.