From 60bdbc33abeb9b52c220f8e549c466d9038c2bec Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jun 2024 16:28:22 +0300 Subject: [PATCH] Updated on 2026-08-14 --- common/ui-charts/build.gradle.kts | 1 + .../tangem/common/ui/charts/MarketChart.kt | 402 +++++++++++++++++- .../ui/charts/layer/MarketChartLayer.kt | 168 +++++--- .../common/ui/charts/marker/ChartMarker.kt | 23 +- .../preview/MarketChartPreviewDataProvider.kt | 152 +++++++ .../common/ui/charts/preview/PreviewData.kt | 77 ---- .../ui/charts/state/AxisLabelFormatter.kt | 17 + .../common/ui/charts/state/MarketChartData.kt | 36 ++ .../charts/state/MarketChartDataProducer.kt | 207 +++++++++ .../common/ui/charts/state/MarketChartLook.kt | 28 ++ .../ui/charts/state/MarketChartState.kt | 151 +++++++ core/ui/build.gradle.kts | 2 +- gradle/dependencies.toml | 2 +- settings.gradle.kts | 2 +- 14 files changed, 1124 insertions(+), 144 deletions(-) create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt delete mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt diff --git a/common/ui-charts/build.gradle.kts b/common/ui-charts/build.gradle.kts index 62c6c0de87..319c15eebc 100644 --- a/common/ui-charts/build.gradle.kts +++ b/common/ui-charts/build.gradle.kts @@ -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) diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt index 31f0f55f2b..9bbaeaa48b 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -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() -} \ No newline at end of file +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) { + 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) { + 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 { + 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 { + 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>, +) { + 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(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 \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt index 239f0f0665..b224a1838d 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt @@ -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, 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>() - val model = getPreviewBigCartData(xListKey) +private fun LayerChartPreview( + @PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair, List>, +) { + 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), diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt index 4675085e2a..e27ce63a70 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt @@ -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>, +) { val marker = rememberTangemChartMarker(Color.Red, Color.White) - val xListKey = ExtraStore.Key>() - 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 } diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt new file mode 100644 index 0000000000..662cb41bc6 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt @@ -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, List>> { + override val values: Sequence, List>> + 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( + 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) + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt deleted file mode 100644 index 0b90226888..0000000000 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt +++ /dev/null @@ -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>): 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 = 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) - }, - ) -} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt new file mode 100644 index 0000000000..e01681994c --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt @@ -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 +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt new file mode 100644 index 0000000000..7e4887fedc --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt @@ -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 = listOf(), + val y: List = listOf(), + ) : MarketChartData +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt new file mode 100644 index 0000000000..da62140e59 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt @@ -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() + internal val dataState = MutableStateFlow(initialData) + internal val lookState = MutableStateFlow(initialLook) + internal val entries = MutableStateFlow>(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>() + internal val xKey = ExtraStore.Key>() + internal val yKey = ExtraStore.Key>() + + 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() + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt new file mode 100644 index 0000000000..c9ecaae2c0 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt @@ -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, + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt new file mode 100644 index 0000000000..110161db69 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt @@ -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, + 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) { + 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) { + 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): Pair? { + 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 + } +} \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index eb2b8502e1..d4f811162d 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -41,7 +41,7 @@ dependencies { implementation(deps.compose.shimmer) implementation(deps.kotlin.immutable.collections) implementation(deps.zxing.qrCore) - implementation(deps.jodatime) + api(deps.jodatime) implementation(deps.timber) implementation(deps.markdown) } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 7fb3f2a8f0..b549e9bf7b 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -91,7 +91,7 @@ tangemBlockchainSdk = "develop-654" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-360" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ -tangemVico = "2.0.0-alpha.21-tangem13" +tangemVico = "2.0.0-alpha.21-tangem14" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem diff --git a/settings.gradle.kts b/settings.gradle.kts index 82e7f8bef8..914e2ac058 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,11 +35,11 @@ dependencyResolutionManagement { mavenLocal { content { includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin") + includeGroupAndSubgroups("com.tangem.vico") includeModule("com.tangem", "blstlib") includeModule("com.tangem", "blockchain") includeModule("com.tangem", "wallet-core-proto") includeModule("com.tangem", "wallet-core") - includeModule("com.tangem", "vico") } } maven {