Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-14 16:28:22 +03:00
parent 5f68c41da2
commit 60bdbc33ab
14 changed files with 1124 additions and 144 deletions

View file

@ -17,6 +17,7 @@ dependencies {
implementation(deps.tangem.vico.compose)
implementation(deps.tangem.vico.compose.m3)
implementation(deps.lifecycle.compose)
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.ui.tooling)

View file

@ -1,8 +1,402 @@
package com.tangem.common.ui.charts
import androidx.compose.runtime.Composable
import android.content.res.Configuration
import androidx.annotation.FloatRange
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontSynthesis
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.resolveAsTypeface
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.patrykandpatrick.vico.compose.cartesian.*
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberCustomStartAxis
import com.patrykandpatrick.vico.compose.common.of
import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout
import com.patrykandpatrick.vico.core.cartesian.Zoom
import com.patrykandpatrick.vico.core.cartesian.axis.*
import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener
import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget
import com.patrykandpatrick.vico.core.common.Dimensions
import com.patrykandpatrick.vico.core.common.component.LineComponent
import com.patrykandpatrick.vico.core.common.shape.Shape
import com.tangem.common.ui.charts.layer.rememberMarketChartLayer
import com.tangem.common.ui.charts.marker.rememberTangemChartMarker
import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider
import com.tangem.common.ui.charts.state.*
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toTimeFormat
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
private const val GUIDELINES_COUNT = 3
/**
* MarketChart ui component for representing coin prices.
*
* @param modifier The modifier to be applied to the chart.
* @param state The state of the Market Chart, which includes data and look of the chart.
* @param splitChartSegmentColor The color of the grayed by marker chart segment.
* @param backgroundSplitChartSegmentColorAlpha The alpha of the background the [splitChartSegmentColor]
* @param backgroundColorAlpha The alpha of the background color of the chart.
* @param noChartContent A composable function that defines the content to be displayed when there is no data to display.
*/
@Composable
fun MarketChart(
modifier: Modifier = Modifier,
state: MarketChartState = rememberMarketChartState(),
splitChartSegmentColor: Color,
@FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float,
@FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float,
noChartContent: @Composable BoxScope.() -> Unit,
) {
var canvasWidth by remember { mutableIntStateOf(0) }
var canvasHeight by remember { mutableIntStateOf(0) }
val layer = rememberLayerFromState(
state = state,
splitChartSegmentColor = splitChartSegmentColor,
backgroundColorAlpha = backgroundColorAlpha,
backgroundSplitChartSegmentColorAlpha = backgroundSplitChartSegmentColorAlpha,
canvasHeight = canvasHeight,
)
val chart = rememberCartesianChart(
layer,
startAxis = rememberMarketChartStartAxis(
yValueFormatter = state.yValueFormatter,
),
bottomAxis = rememberMarketChartBottomAxis(
xValueFormatter = state.xValueFormatter,
),
)
val marker = rememberTangemChartMarker(
color = state.chartColor,
innerCircleColor = Color.White,
)
val density = LocalDensity.current
CartesianChartHost(
modifier = modifier.onGloballyPositioned {
with(density) {
canvasWidth = it.size.width
canvasHeight = if (it.size.height != 0) {
// FIXME get height bounded to min max chart points
it.size.height - 20.dp.toPx().toInt() - 27.dp.toPx().toInt()
} else {
0
}
}
},
chart = chart,
modelProducer = state.modelProducer,
scrollState = rememberVicoScrollState(scrollEnabled = false),
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
horizontalLayout = HorizontalLayout.fullWidth(),
markerVisibilityListener = state.rememberMarketVisibilityListener(canvasWidth = canvasWidth),
diffAnimationSpec = null,
marker = marker,
placeholder = noChartContent,
)
}
@Composable
fun MarketChart() {
TODO()
}
private fun MarketChartState.rememberMarketVisibilityListener(canvasWidth: Int): CartesianMarkerVisibilityListener {
val state = this
return remember(state.markerVisibilityListener, canvasWidth) {
val maxCanvasXFloat = canvasWidth.toFloat().takeIf { it != 0f }
object : CartesianMarkerVisibilityListener {
override fun onShown(marker: CartesianMarker, targets: List<CartesianMarker.Target>) {
state.stopDrawingAnimation()
val xCanvas = (targets[0] as LineCartesianLayerMarkerTarget).canvasX
state.markerFraction = maxCanvasXFloat?.let { xCanvas / it }
state.markerVisibilityListener.onShown(marker, targets)
}
override fun onHidden(marker: CartesianMarker) {
state.markerFraction = null
state.markerVisibilityListener.onHidden(marker)
}
override fun onUpdated(marker: CartesianMarker, targets: List<CartesianMarker.Target>) {
val xCanvas = (targets[0] as LineCartesianLayerMarkerTarget).canvasX
state.markerFraction = maxCanvasXFloat?.let { xCanvas / it }
state.markerVisibilityListener.onUpdated(marker, targets)
}
}
}
}
@Composable
private fun rememberLayerFromState(
state: MarketChartState,
splitChartSegmentColor: Color,
@FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float,
@FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float,
canvasHeight: Int,
): LineCartesianLayer {
return rememberMarketChartLayer(
lineColor = state.chartColor,
backgroundLineColor = state.chartColor.copy(alpha = backgroundColorAlpha),
secondLineColor = splitChartSegmentColor,
backgroundSecondLineColor = splitChartSegmentColor.copy(alpha = backgroundSplitChartSegmentColorAlpha),
secondColorOnTheRightSide = state.markerHighlightRightSide.not(),
startDrawingAnimation = state.startDrawingAnimationState,
markerFraction = state.markerFraction,
axisValueOverrider = AxisValueOverrider.adaptiveYValues(yFraction = 1.2f, round = true), // FIXME ?
canvasHeight = canvasHeight,
)
}
@Composable
private fun rememberMarketChartStartAxis(
yValueFormatter: CartesianValueFormatter,
): VerticalAxis<AxisPosition.Vertical.Start> {
return rememberCustomStartAxis(
axis = null,
tick = null,
guideline = null,
labelGuideline = rememberChartAxisGuidelineComponent(
color = TangemTheme.colors.icon.inactive.copy(alpha = 0.12f),
),
label = rememberAxisLabelComponent(
color = TangemTheme.colors.text.tertiary,
background = null,
padding = Dimensions.of(
start = TangemTheme.dimens.spacing4,
end = TangemTheme.dimens.spacing4,
),
textSize = TangemTheme.typography.caption2.fontSize,
typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(),
),
horizontalLabelPosition = VerticalAxis.HorizontalLabelPosition.Inside,
verticalLabelPosition = VerticalAxis.VerticalLabelPosition.Center,
itemPlacer = AxisItemPlacer.Vertical.count({ GUIDELINES_COUNT }, false),
valueFormatter = yValueFormatter,
)
}
@Composable
fun rememberMarketChartBottomAxis(
xValueFormatter: CartesianValueFormatter,
): HorizontalAxis<AxisPosition.Horizontal.Bottom> {
return rememberBottomAxis(
label = rememberAxisLabelComponent(
color = TangemTheme.colors.text.tertiary,
textSize = TangemTheme.typography.caption2.fontSize,
padding = Dimensions.of(top = TangemTheme.dimens.spacing20),
typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(),
),
tick = null,
axis = null,
guideline = null,
sizeConstraint = BaseAxis.SizeConstraint.Exact(sizeDp = 37f), // FIXME ?
itemPlacer = remember {
AxisItemPlacer.Horizontal.default(
spacing = 25, // FIXME ?
offset = 60, // FIXME ?
shiftExtremeTicks = false,
addExtremeLabelPadding = false,
)
},
valueFormatter = xValueFormatter,
)
}
@Composable
private fun rememberChartAxisGuidelineComponent(color: Color): LineComponent {
return rememberAxisGuidelineComponent(
color = color,
shape = Shape.Rectangle,
margins = Dimensions(
startDp = TangemTheme.dimens.spacing4.value,
endDp = TangemTheme.dimens.spacing4.value,
topDp = 0f,
bottomDp = 0f,
),
thickness = TangemTheme.dimens.size2,
)
}
@Composable
internal fun TextStyle.toGraphicsTypeFace(): android.graphics.Typeface {
val resolver = LocalFontFamilyResolver.current
return remember(resolver, this) {
resolver.resolveAsTypeface(
fontFamily = this.fontFamily,
fontWeight = this.fontWeight ?: FontWeight.Normal,
fontStyle = this.fontStyle ?: FontStyle.Normal,
fontSynthesis = this.fontSynthesis ?: FontSynthesis.All,
)
}.value
}
// region Preview
@Suppress("LongMethod")
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun MarketChartPreview(
@PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair<List<BigDecimal>, List<BigDecimal>>,
) {
val y = previewData.second
val x = previewData.first
val dataProducer = remember {
MarketChartDataProducer.build {
chartLook = MarketChartLook(
type = MarketChartLook.Type.Growing,
markerHighlightRightSide = true,
animationOnDataChange = true,
)
}
}
LaunchedEffect(key1 = Unit) {
dataProducer.runTransactionSuspend {
chartData = MarketChartData.Data(
x = x,
y = y,
)
updateLook {
it.copy(
xAxisFormatter = { value ->
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd)
},
yAxisFormatter = { value ->
value.setScale(3, RoundingMode.HALF_UP).toPlainString()
},
)
}
}
}
var markerPoint by remember {
mutableStateOf(Pair<BigDecimal?, BigDecimal?>(null, null))
}
val coroutineScope = rememberCoroutineScope()
val look by dataProducer.lookState.collectAsState()
TangemThemePreview {
val growingColor = TangemTheme.colors.icon.accent
val fallingColor = TangemTheme.colors.icon.warning
val chartState = rememberMarketChartState(
dataProducer = dataProducer,
onMarkerShown = { x, y ->
markerPoint = Pair(x, y)
},
colorMapper = {
when (it) {
MarketChartLook.Type.Growing -> growingColor
MarketChartLook.Type.Falling -> fallingColor
}
},
)
Column(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.fillMaxWidth(),
) {
Text(text = "Point: ${markerPoint.first}, ${markerPoint.second}")
MarketChart(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors.background.tertiary)
.height(173.dp),
state = chartState,
splitChartSegmentColor = TangemTheme.colors.icon.inactive,
backgroundSplitChartSegmentColorAlpha = 0.24f,
backgroundColorAlpha = 0.24f,
noChartContent = { },
)
SpacerH16()
Button(onClick = { chartState.startDrawingAnimation() }) {
Text("Start drawing animation")
}
Button(
onClick = {
dataProducer.runTransaction {
updateLook {
it.copy(markerHighlightRightSide = !it.markerHighlightRightSide)
}
}
},
) {
Text(
text = "Change marker highlight side",
)
}
Button(onClick = {
coroutineScope.launch {
dataProducer.runTransactionSuspend {
updateData {
MarketChartData.Data(
x = it.x,
y = it.y.reversed(),
)
}
}
}
},) {
Text("Change Data")
}
Button(onClick = {
dataProducer.runTransaction {
updateLook { it.copy(animationOnDataChange = it.animationOnDataChange.not()) }
}
},) {
Text("Change animationOnDataChange = ${look.animationOnDataChange}")
}
Button(onClick = {
dataProducer.runTransaction {
updateLook {
it.copy(
type = if (it.type == MarketChartLook.Type.Growing) {
MarketChartLook.Type.Falling
} else {
MarketChartLook.Type.Growing
},
)
}
}
},) {
Text("Change color type")
}
}
}
}
// endregion Preview

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
import com.patrykandpatrick.vico.compose.cartesian.fullWidth
@ -27,13 +28,15 @@ import com.patrykandpatrick.vico.compose.common.shader.BrushShader
import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout
import com.patrykandpatrick.vico.core.cartesian.Zoom
import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
import com.patrykandpatrick.vico.core.common.data.ExtraStore
import com.patrykandpatrick.vico.core.common.shader.ColorShader
import com.patrykandpatrick.vico.core.common.shader.DynamicShader
import com.tangem.common.ui.charts.preview.getPreviewBigCartData
import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import java.math.BigDecimal
/**
* Creates and remembers a LineCartesianLayer for a chart with specific characteristics.
@ -51,19 +54,21 @@ import com.tangem.core.ui.res.TangemThemePreview
*/
@Suppress("LongParameterList")
@Composable
fun rememberMarketChartLayer(
internal fun rememberMarketChartLayer(
lineColor: Color,
backgroundLineColor: Color,
secondLineColor: Color,
backgroundSecondLineColor: Color,
startDrawingAnimation: MutableState<Boolean>,
axisValueOverrider: AxisValueOverrider,
secondColorOnTheRightSide: Boolean = false,
@FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null,
secondColorOnTheRightSide: Boolean,
@FloatRange(from = 0.0, to = 1.0) markerFraction: Float?,
canvasHeight: Int,
): LineCartesianLayer {
var animationFraction: Float? by remember { mutableStateOf(null) }
LaunchedEffect(startDrawingAnimation.value) {
animationFraction = null
if (startDrawingAnimation.value) {
animate(
initialValue = 0f,
@ -89,9 +94,11 @@ fun rememberMarketChartLayer(
secondColorOnTheRightSide = secondColorOnTheRightSide,
markerFraction = markerFraction,
animationFraction = animationFraction,
canvasHeight = canvasHeight,
)
}
@Suppress("LongParameterList")
@Composable
private fun rememberRawMarketChartLayer(
lineColor: Color,
@ -99,6 +106,7 @@ private fun rememberRawMarketChartLayer(
secondLineColor: Color,
backgroundSecondLineColor: Color,
axisValueOverrider: AxisValueOverrider,
canvasHeight: Int,
secondColorOnTheRightSide: Boolean = false,
@FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null,
@FloatRange(from = 0.0, to = 1.0) animationFraction: Float? = null,
@ -109,38 +117,36 @@ private fun rememberRawMarketChartLayer(
val markerSet = markerFraction != null
val animationRunning = animationFraction != null && animationFraction != 1f
val layerColors = remember(animationRunning, markerSet, secondColorOnTheRightSide) {
when {
!animationRunning && markerSet && secondColorOnTheRightSide -> {
LayerColors(
lineColor = lineColor,
backLineColor = backgroundColorLineGradient,
lineColorRight = secondLineColor,
backLineColorRight = backgroundSecondLineColorGradient,
)
}
!animationRunning && markerSet && !secondColorOnTheRightSide -> {
LayerColors(
lineColor = secondLineColor,
backLineColor = backgroundSecondLineColorGradient,
lineColorRight = lineColor,
backLineColorRight = backgroundColorLineGradient,
)
}
animationRunning -> {
LayerColors(
lineColor = lineColor,
backLineColor = backgroundColorLineGradient,
lineColorRight = Color.Transparent,
backLineColorRight = listOf(Color.Transparent, Color.Transparent),
)
}
else -> {
LayerColors(
lineColor = lineColor,
backLineColor = backgroundColorLineGradient,
)
}
val layerColors = when {
!animationRunning && markerSet && secondColorOnTheRightSide -> {
LayerColors(
lineColor = lineColor,
backLineColor = backgroundColorLineGradient,
lineColorRight = secondLineColor,
backLineColorRight = backgroundSecondLineColorGradient,
)
}
!animationRunning && markerSet && !secondColorOnTheRightSide -> {
LayerColors(
lineColor = secondLineColor,
backLineColor = backgroundSecondLineColorGradient,
lineColorRight = lineColor,
backLineColorRight = backgroundColorLineGradient,
)
}
animationRunning -> {
LayerColors(
lineColor = lineColor,
backLineColor = backgroundColorLineGradient,
lineColorRight = Color.Transparent,
backLineColorRight = listOf(Color.Transparent, Color.Transparent),
)
}
else -> {
LayerColors(
lineColor = lineColor,
backLineColor = backgroundColorLineGradient,
)
}
}
@ -148,6 +154,7 @@ private fun rememberRawMarketChartLayer(
fractionValue = animationFraction ?: markerFraction,
axisValueOverrider = axisValueOverrider,
layerColors = layerColors,
canvasHeight = canvasHeight,
)
}
@ -163,14 +170,26 @@ private fun rememberLayer(
fractionValue: Float?,
axisValueOverrider: AxisValueOverrider,
layerColors: LayerColors,
canvasHeight: Int,
): LineCartesianLayer {
val endGradientColorPosition = if (canvasHeight != 0) {
canvasHeight * END_GRADIENT_COLOR_POSITION_PERCENTAGE
} else {
Float.POSITIVE_INFINITY
}
return rememberLineCartesianLayer(
listOf(
if (layerColors.lineColorRight == null || layerColors.backLineColorRight == null || fractionValue == null) {
rememberLineSpec(
shader = remember(layerColors.lineColor) { ColorShader(color = layerColors.lineColor.toArgb()) },
backgroundShader = remember(layerColors.backLineColor) {
BrushShader(brush = Brush.verticalGradient(layerColors.backLineColor))
backgroundShader = remember(layerColors.backLineColor, endGradientColorPosition) {
BrushShader(
brush = Brush.verticalGradient(
colors = layerColors.backLineColor,
endY = endGradientColorPosition,
),
)
},
)
} else {
@ -181,11 +200,21 @@ private fun rememberLayer(
positions = floatArrayOf(fractionValue, fractionValue),
)
},
backgroundShaderFirst = remember(layerColors.backLineColor) {
BrushShader(brush = Brush.verticalGradient(layerColors.backLineColor))
backgroundShaderFirst = remember(layerColors.backLineColor, endGradientColorPosition) {
BrushShader(
brush = Brush.verticalGradient(
colors = layerColors.backLineColor,
endY = endGradientColorPosition,
),
)
},
backgroundShaderSecond = remember(layerColors.backLineColorRight) {
BrushShader(brush = Brush.verticalGradient(layerColors.backLineColorRight))
backgroundShaderSecond = remember(layerColors.backLineColorRight, endGradientColorPosition) {
BrushShader(
brush = Brush.verticalGradient(
colors = layerColors.backLineColorRight,
endY = endGradientColorPosition,
),
)
},
xSplitFraction = fractionValue,
)
@ -195,14 +224,23 @@ private fun rememberLayer(
)
}
private const val END_GRADIENT_COLOR_POSITION_PERCENTAGE = 0.9f
// region Preview
@Suppress("LongMethod")
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun LayerChartPreview() {
val xListKey = ExtraStore.Key<List<Long>>()
val model = getPreviewBigCartData(xListKey)
private fun LayerChartPreview(
@PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair<List<BigDecimal>, List<BigDecimal>>,
) {
val y = previewData.second.map { it.toFloat() }
val x = List(y.size) { it.toFloat() }
val model = CartesianChartModel(LineCartesianLayerModel.build { series(x, y) })
var lineColor by remember {
mutableStateOf(Color.Blue)
}
TangemThemePreview {
Column(
@ -213,12 +251,13 @@ private fun LayerChartPreview() {
modifier = Modifier.fillMaxWidth(),
chart = rememberCartesianChart(
rememberRawMarketChartLayer(
lineColor = Color.Blue,
backgroundLineColor = Color.Blue.copy(alpha = 0.24f),
lineColor = lineColor,
backgroundLineColor = lineColor.copy(alpha = 0.24f),
secondLineColor = Color.Gray,
backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f),
secondColorOnTheRightSide = true,
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
canvasHeight = 495,
),
),
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
@ -230,13 +269,14 @@ private fun LayerChartPreview() {
modifier = Modifier.fillMaxWidth(),
chart = rememberCartesianChart(
rememberRawMarketChartLayer(
lineColor = Color.Blue,
backgroundLineColor = Color.Blue.copy(alpha = 0.24f),
lineColor = lineColor,
backgroundLineColor = lineColor.copy(alpha = 0.24f),
secondLineColor = Color.Gray,
backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f),
markerFraction = 0.35f,
secondColorOnTheRightSide = true,
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
canvasHeight = 495,
),
),
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
@ -248,14 +288,34 @@ private fun LayerChartPreview() {
modifier = Modifier.fillMaxWidth(),
chart = rememberCartesianChart(
rememberRawMarketChartLayer(
lineColor = Color.Blue,
backgroundLineColor = Color.Blue.copy(alpha = 0.24f),
secondLineColor = Color.Blue,
backgroundSecondLineColor = Color.Blue.copy(alpha = 0.24f),
lineColor = lineColor,
backgroundLineColor = lineColor.copy(alpha = 0.24f),
secondLineColor = Color.Gray,
backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f),
markerFraction = 0.35f,
secondColorOnTheRightSide = false,
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
canvasHeight = 495,
),
),
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),
horizontalLayout = HorizontalLayout.fullWidth(),
model = model,
)
CartesianChartHost(
modifier = Modifier.fillMaxWidth(),
chart = rememberCartesianChart(
rememberRawMarketChartLayer(
lineColor = lineColor,
backgroundLineColor = lineColor.copy(alpha = 0.24f),
secondLineColor = lineColor,
backgroundSecondLineColor = lineColor.copy(alpha = 0.24f),
markerFraction = 0.35f,
secondColorOnTheRightSide = true,
animationFraction = 0.7f,
axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY),
canvasHeight = 495,
),
),
zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false),

View file

@ -9,6 +9,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
import com.patrykandpatrick.vico.compose.cartesian.fullWidth
@ -24,16 +25,18 @@ import com.patrykandpatrick.vico.compose.common.shader.color
import com.patrykandpatrick.vico.compose.common.shape.dashed
import com.patrykandpatrick.vico.core.cartesian.*
import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker
import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker
import com.patrykandpatrick.vico.core.common.Dimensions
import com.patrykandpatrick.vico.core.common.component.TextComponent
import com.patrykandpatrick.vico.core.common.data.ExtraStore
import com.patrykandpatrick.vico.core.common.shader.DynamicShader
import com.patrykandpatrick.vico.core.common.shape.Shape
import com.tangem.common.ui.charts.preview.getPreviewBigCartData
import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import java.math.BigDecimal
/**
* @param color The color of the indicator and guideline.
@ -53,7 +56,11 @@ internal fun rememberTangemChartMarker(color: Color, innerCircleColor: Color): C
)
val indicatorRearComponent = rememberShapeComponent(
shape = Shape.Pill,
color = color.copy(alpha = INDICATOR_REAR_COLOR_ALPHA),
color = if (color == Color.Transparent) {
Color.Transparent
} else {
color.copy(alpha = INDICATOR_REAR_COLOR_ALPHA)
},
)
val indicator = rememberLayeredComponent(
rear = indicatorRearComponent,
@ -105,10 +112,14 @@ private const val INDICATOR_REAR_COLOR_ALPHA = .24f
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemChartMarkerPreview() {
private fun TangemChartMarkerPreview(
@PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair<List<BigDecimal>, List<BigDecimal>>,
) {
val marker = rememberTangemChartMarker(Color.Red, Color.White)
val xListKey = ExtraStore.Key<List<Long>>()
val model = getPreviewBigCartData(xListKey)
val y = previewData.second.map { it.toFloat() }
val x = List(y.size) { it.toFloat() }
val model = CartesianChartModel(LineCartesianLayerModel.build { series(x, y) })
val centerAprx = (model.models[0].minX + model.models[0].maxX) / 2f
val center = model.models[0].getXDeltaGcd().let { centerAprx - centerAprx % it }

View file

@ -0,0 +1,152 @@
@file:Suppress("MagicNumber")
package com.tangem.common.ui.charts.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import java.math.BigDecimal
internal class MarketChartPreviewDataProvider : PreviewParameterProvider<Pair<List<BigDecimal>, List<BigDecimal>>> {
override val values: Sequence<Pair<List<BigDecimal>, List<BigDecimal>>>
get() {
val bitcoinPrice = listOf(
59270, 61748, 61941, 62889, 62740, 62989, 63858, 63608, 63951, 63917,
63253, 63765, 63862, 64502, 63876, 64028, 63875, 64531, 64249, 63680,
63228, 63159, 63249, 63664, 63469, 63711, 63003, 62333, 62882, 62183,
62231, 62244, 62200, 61188, 61683, 61199, 61036, 62116, 62436, 63065,
62908, 63067, 63294, 60904, 60677, 60790, 60705, 61041, 60667, 61158,
61112, 60795, 60900, 60771, 61124, 61343, 61371, 61484, 61133, 62372,
62704, 63007, 63092, 62905, 62470, 62019, 61756, 61776, 61557, 61540,
61904, 62151, 62419, 64663, 65955, 66229, 65991, 66176, 66517, 65838,
65210, 65216, 65611, 66467, 66209, 67247, 66913, 67059, 66978, 66845,
67240, 66874, 66993, 66940, 67185, 67330, 67340, 66845, 66062, 66273,
66645, 66865, 67005, 67382, 70049, 71464, 71293, 70875, 71137, 69720,
69323, 70139, 69964, 69716, 69855, 70442, 69774, 69125, 69404, 69714,
69967, 68042, 67077, 67938, 67833, 67182, 67306, 68327, 69093, 68517,
68726, 68759, 69061, 68880, 69148, 69305, 69044, 69313, 69122, 68821,
68854, 68506, 68823, 68613, 68420, 70356, 69241, 69401, 68004, 67630,
68311, 68311, 68291, 68302, 68717, 67926, 67690, 67314, 67285, 67567,
68026, 67552, 67740, 68519, 68611, 68363, 68503, 68171, 68326, 67121,
67626, 67481, 67657, 67567, 67608, 67607, 67683, 67729, 67733, 67709,
).map { it.toBigDecimal() }
val bitcoinTimestamps = listOf(
1714752000000, 1714766400000, 1714780800000, 1714795200000, 1714809600000,
1714824000000, 1714838400000, 1714852800000, 1714867200000, 1714881600000,
1714896000000, 1714910400000, 1714924800000, 1714939200000, 1714953600000,
1714968000000, 1714982400000, 1714996800000, 1715011200000, 1715025600000,
1715040000000, 1715054400000, 1715068800000, 1715083200000, 1715097600000,
1715112000000, 1715126400000, 1715140800000, 1715155200000, 1715169600000,
1715184000000, 1715198400000, 1715212800000, 1715227200000, 1715241600000,
1715256000000, 1715270400000, 1715284800000, 1715299200000, 1715313600000,
1715328000000, 1715342400000, 1715356800000, 1715371200000, 1715385600000,
1715400000000, 1715414400000, 1715428800000, 1715443200000, 1715457600000,
1715472000000, 1715486400000, 1715500800000, 1715515200000, 1715529600000,
1715544000000, 1715558400000, 1715572800000, 1715587200000, 1715601600000,
1715616000000, 1715630400000, 1715644800000, 1715659200000, 1715673600000,
1715688000000, 1715702400000, 1715716800000, 1715731200000, 1715745600000,
1715760000000, 1715774400000, 1715788800000, 1715803200000, 1715817600000,
1715832000000, 1715846400000, 1715860800000, 1715875200000, 1715889600000,
1715904000000, 1715918400000, 1715932800000, 1715947200000, 1715961600000,
1715976000000, 1715990400000, 1716004800000, 1716019200000, 1716033600000,
1716048000000, 1716062400000, 1716076800000, 1716091200000, 1716105600000,
1716120000000, 1716134400000, 1716148800000, 1716163200000, 1716177600000,
1716192000000, 1716206400000, 1716220800000, 1716235200000, 1716249600000,
1716264000000, 1716278400000, 1716292800000, 1716307200000, 1716321600000,
1716336000000, 1716350400000, 1716364800000, 1716379200000, 1716393600000,
1716408000000, 1716422400000, 1716436800000, 1716451200000, 1716465600000,
1716480000000, 1716494400000, 1716508800000, 1716523200000, 1716537600000,
1716552000000, 1716566400000, 1716580800000, 1716595200000, 1716609600000,
1716624000000, 1716638400000, 1716652800000, 1716667200000, 1716681600000,
1716696000000, 1716710400000, 1716724800000, 1716739200000, 1716753600000,
1716768000000, 1716782400000, 1716796800000, 1716811200000, 1716825600000,
1716840000000, 1716854400000, 1716868800000, 1716883200000, 1716897600000,
1716912000000, 1716926400000, 1716940800000, 1716955200000, 1716969600000,
1716984000000, 1716998400000, 1717012800000, 1717027200000, 1717041600000,
1717056000000, 1717070400000, 1717084800000, 1717099200000, 1717113600000,
1717128000000, 1717142400000, 1717156800000, 1717171200000, 1717185600000,
1717200000000, 1717214400000, 1717228800000, 1717243200000, 1717257600000,
1717272000000, 1717286400000, 1717300800000, 1717315200000, 1717329600000,
).map { it.toBigDecimal() }
val notcoinPrice = listOf(
"0.02026708", "0.02033274", "0.02112643", "0.02090579",
"0.02047231", "0.0215645", "0.0089055", "0.00674459",
"0.00733586", "0.00758165", "0.0068592", "0.00680505",
"0.00685493", "0.00702113", "0.00725545", "0.00696515",
"0.00681556", "0.00677416", "0.00669383", "0.00659057",
"0.00668081", "0.0066211", "0.0065232", "0.00612542",
"0.00600531", "0.00570974", "0.00559875", "0.00555247",
"0.005495", "0.00547841", "0.00545514", "0.00553865",
"0.00563302", "0.00568475", "0.00562512", "0.00551265",
"0.005398", "0.00560001", "0.00572701", "0.00563223",
"0.00555209", "0.00549261", "0.00524999", "0.00531381",
"0.00539702", "0.00530647", "0.00533963", "0.00527143",
"0.00525981", "0.00495323", "0.00481584", "0.0048854",
"0.00480298", "0.00471316", "0.00473191", "0.00476389",
"0.00476992", "0.00484649", "0.00471095", "0.00501396",
"0.00495481", "0.00545029", "0.0053947", "0.00533251",
"0.00518064", "0.00504481", "0.00507209", "0.00516952",
"0.00524886", "0.00542661", "0.00544231", "0.00579898",
"0.00681472", "0.00720634", "0.00824023", "0.00856427",
"0.00821144", "0.00818103", "0.00960254", "0.00911172",
"0.00888481", "0.00925911", "0.0091132", "0.009285",
"0.00886256", "0.00938211", "0.00936566", "0.0104387",
"0.01088452", "0.01200575", "0.01217514", "0.011921",
"0.01293908", "0.0126124", "0.01224851", "0.01191149",
"0.01179783", "0.01164533", "0.01175718", "0.01169137",
"0.01212768", "0.01215952", "0.01300256", "0.01589522",
"0.01588223", "0.01780629", "0.01919794", "0.01922187",
"0.02165268", "0.02400163", "0.02290975", "0.02383495",
"0.02088105", "0.02373489", "0.02269442", "0.02226249",
"0.02148038", "0.0232045", "0.02623129", "0.02378975",
"0.02438442", "0.02417507", "0.02269891", "0.02236198",
"0.02209164", "0.02169842", "0.02137774", "0.02188837",
"0.0216619", "0.02238334", "0.02186701", "0.02186521",
"0.0217595", "0.02099916", "0.02129634", "0.02143028",
"0.02192513", "0.02172005", "0.02184525", "0.01873974",
"0.01899164", "0.01957115", "0.0204723", "0.0199247",
"0.01935441", "0.01886976", "0.01856944", "0.0179975",
"0.0178625", "0.01796981",
).map { BigDecimal(it) }
val notcoinTimestamps = listOf<Long>(
1715428800000, 1715443200000, 1715457600000, 1715472000000,
1715486400000, 1715860800000, 1715875200000, 1715889600000,
1715904000000, 1715918400000, 1715932800000, 1715947200000,
1715961600000, 1715976000000, 1715990400000, 1716004800000,
1716019200000, 1716033600000, 1716048000000, 1716062400000,
1716076800000, 1716091200000, 1716105600000, 1716120000000,
1716134400000, 1716148800000, 1716163200000, 1716177600000,
1716192000000, 1716206400000, 1716220800000, 1716235200000,
1716249600000, 1716264000000, 1716278400000, 1716292800000,
1716307200000, 1716321600000, 1716336000000, 1716350400000,
1716364800000, 1716379200000, 1716393600000, 1716408000000,
1716422400000, 1716436800000, 1716451200000, 1716465600000,
1716480000000, 1716494400000, 1716508800000, 1716523200000,
1716537600000, 1716552000000, 1716566400000, 1716580800000,
1716595200000, 1716609600000, 1716624000000, 1716638400000,
1716652800000, 1716667200000, 1716681600000, 1716696000000,
1716710400000, 1716724800000, 1716739200000, 1716753600000,
1716768000000, 1716782400000, 1716796800000, 1716811200000,
1716825600000, 1716840000000, 1716854400000, 1716868800000,
1716883200000, 1716897600000, 1716912000000, 1716926400000,
1716940800000, 1716955200000, 1716969600000, 1716984000000,
1716998400000, 1717012800000, 1717027200000, 1717041600000,
1717056000000, 1717070400000, 1717084800000, 1717099200000,
1717113600000, 1717128000000, 1717142400000, 1717156800000,
1717171200000, 1717185600000, 1717200000000, 1717214400000,
1717228800000, 1717243200000, 1717257600000, 1717272000000,
1717286400000, 1717300800000, 1717315200000, 1717329600000,
1717344000000, 1717358400000, 1717372800000, 1717387200000,
1717401600000, 1717416000000, 1717430400000, 1717444800000,
1717459200000, 1717473600000, 1717488000000, 1717502400000,
1717516800000, 1717531200000, 1717545600000, 1717560000000,
1717574400000, 1717588800000, 1717603200000, 1717617600000,
1717632000000, 1717646400000, 1717660800000, 1717675200000,
1717689600000, 1717704000000, 1717718400000, 1717732800000,
1717747200000, 1717761600000, 1717776000000, 1717790400000,
1717804800000, 1717819200000, 1717833600000, 1717848000000,
1717862400000, 1717876800000, 1717891200000, 1717905600000,
1717920000000, 1717934400000,
).map { it.toBigDecimal() }
return sequenceOf(bitcoinTimestamps to bitcoinPrice, notcoinTimestamps to notcoinPrice)
}
}

View file

@ -1,77 +0,0 @@
@file:Suppress("MagicNumber", "LongMethod")
package com.tangem.common.ui.charts.preview
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
import com.patrykandpatrick.vico.core.common.data.ExtraStore
import com.patrykandpatrick.vico.core.common.data.MutableExtraStore
internal fun getPreviewBigCartData(xListKey: ExtraStore.Key<List<Long>>): CartesianChartModel {
val y = listOf(
59270, 61748, 61941, 62889, 62740, 62989, 63858, 63608, 63951, 63917,
63253, 63765, 63862, 64502, 63876, 64028, 63875, 64531, 64249, 63680,
63228, 63159, 63249, 63664, 63469, 63711, 63003, 62333, 62882, 62183,
62231, 62244, 62200, 61188, 61683, 61199, 61036, 62116, 62436, 63065,
62908, 63067, 63294, 60904, 60677, 60790, 60705, 61041, 60667, 61158,
61112, 60795, 60900, 60771, 61124, 61343, 61371, 61484, 61133, 62372,
62704, 63007, 63092, 62905, 62470, 62019, 61756, 61776, 61557, 61540,
61904, 62151, 62419, 64663, 65955, 66229, 65991, 66176, 66517, 65838,
65210, 65216, 65611, 66467, 66209, 67247, 66913, 67059, 66978, 66845,
67240, 66874, 66993, 66940, 67185, 67330, 67340, 66845, 66062, 66273,
66645, 66865, 67005, 67382, 70049, 71464, 71293, 70875, 71137, 69720,
69323, 70139, 69964, 69716, 69855, 70442, 69774, 69125, 69404, 69714,
69967, 68042, 67077, 67938, 67833, 67182, 67306, 68327, 69093, 68517,
68726, 68759, 69061, 68880, 69148, 69305, 69044, 69313, 69122, 68821,
68854, 68506, 68823, 68613, 68420, 70356, 69241, 69401, 68004, 67630,
68311, 68311, 68291, 68302, 68717, 67926, 67690, 67314, 67285, 67567,
68026, 67552, 67740, 68519, 68611, 68363, 68503, 68171, 68326, 67121,
67626, 67481, 67657, 67567, 67608, 67607, 67683, 67729, 67733, 67709,
)
val timestamps: List<Long> = listOf(
1714752000000, 1714766400000, 1714780800000, 1714795200000, 1714809600000,
1714824000000, 1714838400000, 1714852800000, 1714867200000, 1714881600000,
1714896000000, 1714910400000, 1714924800000, 1714939200000, 1714953600000,
1714968000000, 1714982400000, 1714996800000, 1715011200000, 1715025600000,
1715040000000, 1715054400000, 1715068800000, 1715083200000, 1715097600000,
1715112000000, 1715126400000, 1715140800000, 1715155200000, 1715169600000,
1715184000000, 1715198400000, 1715212800000, 1715227200000, 1715241600000,
1715256000000, 1715270400000, 1715284800000, 1715299200000, 1715313600000,
1715328000000, 1715342400000, 1715356800000, 1715371200000, 1715385600000,
1715400000000, 1715414400000, 1715428800000, 1715443200000, 1715457600000,
1715472000000, 1715486400000, 1715500800000, 1715515200000, 1715529600000,
1715544000000, 1715558400000, 1715572800000, 1715587200000, 1715601600000,
1715616000000, 1715630400000, 1715644800000, 1715659200000, 1715673600000,
1715688000000, 1715702400000, 1715716800000, 1715731200000, 1715745600000,
1715760000000, 1715774400000, 1715788800000, 1715803200000, 1715817600000,
1715832000000, 1715846400000, 1715860800000, 1715875200000, 1715889600000,
1715904000000, 1715918400000, 1715932800000, 1715947200000, 1715961600000,
1715976000000, 1715990400000, 1716004800000, 1716019200000, 1716033600000,
1716048000000, 1716062400000, 1716076800000, 1716091200000, 1716105600000,
1716120000000, 1716134400000, 1716148800000, 1716163200000, 1716177600000,
1716192000000, 1716206400000, 1716220800000, 1716235200000, 1716249600000,
1716264000000, 1716278400000, 1716292800000, 1716307200000, 1716321600000,
1716336000000, 1716350400000, 1716364800000, 1716379200000, 1716393600000,
1716408000000, 1716422400000, 1716436800000, 1716451200000, 1716465600000,
1716480000000, 1716494400000, 1716508800000, 1716523200000, 1716537600000,
1716552000000, 1716566400000, 1716580800000, 1716595200000, 1716609600000,
1716624000000, 1716638400000, 1716652800000, 1716667200000, 1716681600000,
1716696000000, 1716710400000, 1716724800000, 1716739200000, 1716753600000,
1716768000000, 1716782400000, 1716796800000, 1716811200000, 1716825600000,
1716840000000, 1716854400000, 1716868800000, 1716883200000, 1716897600000,
1716912000000, 1716926400000, 1716940800000, 1716955200000, 1716969600000,
1716984000000, 1716998400000, 1717012800000, 1717027200000, 1717041600000,
1717056000000, 1717070400000, 1717084800000, 1717099200000, 1717113600000,
1717128000000, 1717142400000, 1717156800000, 1717171200000, 1717185600000,
1717200000000, 1717214400000, 1717228800000, 1717243200000, 1717257600000,
1717272000000, 1717286400000, 1717300800000, 1717315200000, 1717329600000,
)
val x = List(y.size) { index -> index }
return CartesianChartModel(LineCartesianLayerModel.build { series(x, y) }).copy(
extraStore = MutableExtraStore().apply {
set(xListKey, timestamps)
},
)
}

View file

@ -0,0 +1,17 @@
package com.tangem.common.ui.charts.state
import androidx.compose.runtime.Stable
import java.math.BigDecimal
/**
* Used for formatting the axis labels in a chart.
* It takes a BigDecimal value and returns a CharSequence that represents the formatted label.
*
* @param value The value to be formatted.
* @return The formatted label as a CharSequence.
*/
@Stable
fun interface AxisLabelFormatter {
fun format(value: BigDecimal): CharSequence
}

View file

@ -0,0 +1,36 @@
package com.tangem.common.ui.charts.state
import androidx.compose.runtime.Immutable
import java.math.BigDecimal
@Immutable
sealed interface MarketChartData {
/**
* This interface represents the state when there is no data for the Market Chart.
*/
@Immutable
sealed interface NoData : MarketChartData {
@Immutable
data object Empty : NoData
@Immutable
data object Loading : NoData
@Immutable
data object ErrorAndRetry : NoData
}
/**
* This data class represents the data for the Market Chart.
* It includes properties for x and y values.
*
* @property x List of x values.
* @property y List of y values.
*/
@Immutable
data class Data(
val x: List<BigDecimal> = listOf(),
val y: List<BigDecimal> = listOf(),
) : MarketChartData
}

View file

@ -0,0 +1,207 @@
package com.tangem.common.ui.charts.state
import androidx.compose.runtime.Stable
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel
import com.patrykandpatrick.vico.core.common.data.ExtraStore
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
import java.math.BigDecimal
/**
* This class represents a transaction for updating the state and look of a Market Chart.
*
* @property chartLook The updated look of the Market Chart.
* @property chartData The updated state of the Market Chart.
*/
class Transaction(
private val currentData: MarketChartData,
private val currentLook: MarketChartLook,
) {
var chartLook: MarketChartLook? = null
var chartData: MarketChartData.NoData? = null
fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) {
chartLook = block(currentLook)
}
fun updateState(block: (prev: MarketChartData) -> MarketChartData.NoData) {
chartData = block(currentData)
}
}
/**
* This class represents a transaction for updating the state and look of a Market Chart.
* It extends the Transaction class and allows update state by data.
*
* @property chartData The updated state of the Market Chart.
* @property chartLook The updated look of the Market Chart.
*/
class TransactionSuspend(
private val currentData: MarketChartData,
private val currentLook: MarketChartLook,
) {
internal var nonSuspendTransaction: Transaction? = null
var chartData: MarketChartData? = null
var chartLook: MarketChartLook?
get() = nonSuspendTransaction?.chartLook
set(value) {
if (nonSuspendTransaction == null) {
nonSuspendTransaction = Transaction(currentData, currentLook)
}
nonSuspendTransaction?.chartLook = value
}
fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) {
chartLook = block(currentLook)
}
internal fun updateState(block: (prev: MarketChartData) -> MarketChartData) {
chartData = block(currentData)
}
internal fun updateData(block: (prev: MarketChartData.Data) -> MarketChartData.Data) {
chartData = when (val currentState = currentData) {
is MarketChartData.Data -> block(currentState)
else -> currentState
}
}
}
@Stable
class MarketChartDataProducer private constructor(
initialData: MarketChartData,
initialLook: MarketChartLook,
private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
) {
internal val startDrawingAnimation = MutableSharedFlow<Unit>()
internal val dataState = MutableStateFlow(initialData)
internal val lookState = MutableStateFlow(initialLook)
internal val entries = MutableStateFlow<List<LineCartesianLayerModel.Entry>>(emptyList())
internal val modelProducer = CartesianChartModelProducer.build(dispatcher = dispatcher)
/**
* This function runs a suspending transaction block to update the state and look of the Market Chart.
*/
suspend fun runTransactionSuspend(block: TransactionSuspend.() -> Unit) =
handleTransactionSuspend(transaction = TransactionSuspend(dataState.value, lookState.value).apply(block))
/**
* This function runs a non-suspending transaction block to update the state and look of the Market Chart.
*/
fun runTransaction(block: Transaction.() -> Unit) =
handleTransaction(transaction = Transaction(dataState.value, lookState.value).apply(block))
private suspend fun handleTransactionSuspend(transaction: TransactionSuspend) {
val nonSuspendTransaction = transaction.nonSuspendTransaction
val chartData = transaction.chartData
val oldData = dataState.value
if (chartData != null) {
dataState.value = chartData
}
if (chartData is MarketChartData.Data && (oldData !is MarketChartData.Data || oldData != chartData)) {
if (lookState.value.animationOnDataChange) {
startDrawingAnimation.emit(Unit)
}
withContext(dispatcher) {
val minX = chartData.x.min()
val minY = chartData.y.min()
val normY = chartData.y.map { normalize(it, minY) }
val normX = chartData.x.map { normalize(it, minX) }
val entriesLocal = normX.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, normY[index]) }
entries.value = entriesLocal
modelProducer.runTransaction {
add(LineCartesianLayerModel.Partial(series = listOf(entriesLocal)))
updateExtras {
it[entriesKey] = entriesLocal
it[xKey] = chartData.x
it[yKey] = chartData.y
}
}.await()
}
}
nonSuspendTransaction?.let { handleTransaction(it) }
}
private fun handleTransaction(transaction: Transaction) {
transaction.chartData?.let {
dataState.value = it
}
transaction.chartLook?.let {
lookState.value = it
}
}
companion object {
internal val entriesKey = ExtraStore.Key<List<LineCartesianLayerModel.Entry>>()
internal val xKey = ExtraStore.Key<List<BigDecimal>>()
internal val yKey = ExtraStore.Key<List<BigDecimal>>()
private val initialData: MarketChartData = MarketChartData.NoData.Empty
private val initialLook: MarketChartLook = MarketChartLook()
/**
* This function builds a MarketChartDataProducer with the given parameters.
* It runs a suspending transaction block to initialize the data and look of the Market Chart.
*
* @param dispatcher The dispatcher to be used for data updates.
* @param block The transaction block to be run.
* @return A MarketChartDataProducer.
*/
suspend fun buildSuspend(
dispatcher: CoroutineDispatcher = Dispatchers.Default,
block: TransactionSuspend.() -> Unit,
): MarketChartDataProducer {
val transaction = TransactionSuspend(initialData, initialLook).apply(block)
return MarketChartDataProducer(
initialData = initialData,
initialLook = initialLook,
dispatcher = dispatcher,
).apply {
handleTransactionSuspend(transaction)
}
}
/**
* This function builds a MarketChartDataProducer with the given parameters.
* It runs a non-suspending transaction block to initialize the data and look of the Market Chart.
*
* @param dispatcher The dispatcher to be used for data updates.
* @param block The transaction block to be run.
* @return A MarketChartDataProducer.
*/
fun build(
dispatcher: CoroutineDispatcher = Dispatchers.Default,
block: Transaction.() -> Unit,
): MarketChartDataProducer {
val transaction = Transaction(initialData, initialLook).apply(block)
return MarketChartDataProducer(
initialData = transaction.chartData ?: initialData,
initialLook = transaction.chartLook ?: initialLook,
dispatcher = dispatcher,
)
}
}
}
private fun normalize(value: BigDecimal, min: BigDecimal, scale: Int = min.scale()): Float {
val n = value - min
return if (scale > 2) {
n.movePointRight(scale - 2).toFloat()
} else {
n.toFloat()
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.common.ui.charts.state
/**
* This class represents the look and feel of a Market Chart.
* It includes properties for type, marker highlight, animation on data change, animate data appearance,
* and formatters for x and y axis.
*
* @property type The type of the chart, can be either Growing or Falling.
* @property markerHighlightRightSide A boolean indicating whether the marker highlights the right side of the chart.
* @property animationOnDataChange A boolean indicating whether to animate on data change.
* @property animateDataAppearance A boolean indicating whether to animate data appearance.
* @property xAxisFormatter A formatter for the x-axis labels.
* @property yAxisFormatter A formatter for the y-axis labels.
*/
data class MarketChartLook(
val type: Type = Type.Growing,
val markerHighlightRightSide: Boolean = true,
val animationOnDataChange: Boolean = false,
val animateDataAppearance: Boolean = false,
val xAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() },
val yAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() },
) {
enum class Type {
Growing,
Falling,
}
}

View file

@ -0,0 +1,151 @@
package com.tangem.common.ui.charts.state
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker
import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener
import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget
import java.math.BigDecimal
/**
* MarketChartState used for MarketChart ui component.
*
* @param dataProducer The producer of the data for the Market Chart.
* @param colorMapper A function that maps a MarketChartLook.Type to a Color.
* @param onMarkerShown A callback function that is called when the marker is shown, hidden, or updated.
* @return A MarketChartState.
*/
@Composable
fun rememberMarketChartState(
dataProducer: MarketChartDataProducer = remember { MarketChartDataProducer.build {} },
colorMapper: (MarketChartLook.Type) -> Color = {
when (it) {
MarketChartLook.Type.Growing -> Color.Green
MarketChartLook.Type.Falling -> Color.Red
}
},
onMarkerShown: (x: BigDecimal?, y: BigDecimal?) -> Unit = { _, _ -> },
): MarketChartState {
val lookState = dataProducer.lookState.collectAsStateWithLifecycle()
val state = remember(dataProducer, lookState, colorMapper, onMarkerShown) {
MarketChartState(dataProducer, lookState, colorMapper, onMarkerShown)
}
LaunchedEffect(Unit) {
dataProducer.startDrawingAnimation.collect {
state.startDrawingAnimation()
}
}
return state
}
/**
* Represents the state of a Market Chart.
*
* @property dataProducer The producer of the data for the Market Chart.
* @property lookState The look state of the Market Chart.
* @property colorMapper A function that maps a MarketChartLook.Type to a Color.
* @property markerCallback A callback function that is called when the marker is shown, hidden, or updated.
* @property isDrawingAnimationInProgress A boolean indicating whether the drawing animation is in progress.
*/
@Stable
class MarketChartState internal constructor(
private val dataProducer: MarketChartDataProducer,
private val lookState: State<MarketChartLook>,
private val colorMapper: (MarketChartLook.Type) -> Color,
private val markerCallback: (x: BigDecimal?, y: BigDecimal?) -> Unit,
) {
internal val startDrawingAnimationState = mutableStateOf(false)
internal val modelProducer = dataProducer.modelProducer
internal val chartColor by derivedStateOf {
colorMapper(lookState.value.type)
}
internal val markerHighlightRightSide by derivedStateOf {
lookState.value.markerHighlightRightSide
}
internal val xValueFormatter by derivedStateOf {
CartesianValueFormatter { value, _, _ ->
val state = dataProducer.dataState.value as? MarketChartData.Data
?: return@CartesianValueFormatter value.toString()
val sMin = state.x.min()
val scale = sMin.scale()
val bVal = if (scale > 2) {
value.toBigDecimal().movePointLeft(scale - 2) + sMin
} else {
value.toBigDecimal() + sMin
}
lookState.value.xAxisFormatter.format(bVal)
}
}
internal val yValueFormatter by derivedStateOf {
CartesianValueFormatter { value, _, _ ->
val state = dataProducer.dataState.value as? MarketChartData.Data
?: return@CartesianValueFormatter value.toString()
val sMin = state.y.min()
val scale = sMin.scale()
val bVal = if (scale > 2) {
value.toBigDecimal().movePointLeft(scale - 2) + sMin
} else {
value.toBigDecimal() + sMin
}
lookState.value.yAxisFormatter.format(bVal)
}
}
internal var markerFraction: Float? by mutableStateOf(null)
internal val markerVisibilityListener = object : CartesianMarkerVisibilityListener {
override fun onShown(marker: CartesianMarker, targets: List<CartesianMarker.Target>) {
val point = getPoint(targets) ?: run {
markerCallback(null, null)
return
}
markerCallback(point.first, point.second)
}
override fun onHidden(marker: CartesianMarker) {
markerCallback(null, null)
}
override fun onUpdated(marker: CartesianMarker, targets: List<CartesianMarker.Target>) {
val point = getPoint(targets) ?: run {
markerCallback(null, null)
return
}
markerCallback(point.first, point.second)
}
}
val isDrawingAnimationInProgress: Boolean by derivedStateOf {
startDrawingAnimationState.value
}
private fun getPoint(targets: List<CartesianMarker.Target>): Pair<BigDecimal, BigDecimal>? {
val entry = (targets[0] as LineCartesianLayerMarkerTarget).points[0].entry
val entryIndex = dataProducer.entries.value.indexOf(entry).takeIf { it != -1 } ?: return null
val state = dataProducer.dataState.value as? MarketChartData.Data ?: return null
val x = state.x.getOrNull(entryIndex) ?: return null
val y = state.y.getOrNull(entryIndex) ?: return null
return x to y
}
fun startDrawingAnimation() {
startDrawingAnimationState.value = true
}
fun stopDrawingAnimation() {
startDrawingAnimationState.value = false
}
}