Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show the selected entry from the vault chart #249

Merged
merged 6 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading