Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-30 16:48:26 +03:00
parent acf110dbcc
commit 6910f74fd9
242 changed files with 866 additions and 732 deletions

View file

@ -90,7 +90,7 @@ fun MarketChart(
backgroundLineColor = state.chartColor.copy(alpha = backgroundColorAlpha),
secondLineColor = splitChartSegmentColor,
backgroundSecondLineColor = splitChartSegmentColor.copy(alpha = backgroundSplitChartSegmentColorAlpha),
secondColorOnTheRightSide = state.markerHighlightRightSide.not(),
secondColorOnTheRightSide = state.shouldMarkerHighlightRightSide.not(),
markerFraction = state.markerFraction,
axisValueOverrider = AxisValueOverrider.fixed(),
canvasHeight = chartHeight,
@ -114,10 +114,10 @@ fun MarketChart(
CartesianChartHost(
modifier = modifier
.onGloballyPositioned {
canvasWidth = it.size.width
chartHeight = if (it.size.height != 0) {
it.size.height - bottomAxisHeight
.onGloballyPositioned { layoutCoordinates ->
canvasWidth = layoutCoordinates.size.width
chartHeight = if (layoutCoordinates.size.height != 0) {
layoutCoordinates.size.height - bottomAxisHeight
} else {
0
}
@ -126,7 +126,7 @@ fun MarketChart(
.drawBehind {
state.markerFraction
state.chartColor
state.markerHighlightRightSide
state.shouldMarkerHighlightRightSide
},
chart = chart,
modelProducer = state.modelProducer,
@ -269,7 +269,7 @@ private fun rememberChartAxisGuidelineComponent(color: Color): LineComponent {
// region Preview
@Suppress("LongMethod")
@Suppress("LongMethod", "NullableToStringCall")
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -283,7 +283,7 @@ private fun MarketChartPreview(
MarketChartDataProducer.build {
chartLook = MarketChartLook(
type = MarketChartLook.Type.Growing,
markerHighlightRightSide = true,
shouldMarkerHighlightRightSide = true,
)
}
}
@ -294,8 +294,8 @@ private fun MarketChartPreview(
x = x.toImmutableList(),
y = y.toImmutableList(),
)
updateLook {
it.copy(
updateLook { marketChartLook ->
marketChartLook.copy(
xAxisFormatter = { value ->
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMdd)
},
@ -322,8 +322,8 @@ private fun MarketChartPreview(
onMarkerShown = { x, y ->
markerPoint = Pair(x, y)
},
colorMapper = {
when (it) {
colorMapper = { type ->
when (type) {
MarketChartLook.Type.Growing -> growingColor
MarketChartLook.Type.Falling -> fallingColor
MarketChartLook.Type.Neutral -> neutralColor
@ -354,7 +354,7 @@ private fun MarketChartPreview(
onClick = {
dataProducer.runTransaction {
updateLook {
it.copy(markerHighlightRightSide = !it.markerHighlightRightSide)
it.copy(shouldMarkerHighlightRightSide = !it.shouldMarkerHighlightRightSide)
}
}
},
@ -367,10 +367,10 @@ private fun MarketChartPreview(
onClick = {
coroutineScope.launch {
dataProducer.runTransactionSuspend {
updateData {
updateData { data ->
MarketChartData.Data(
x = it.x,
y = it.y.reversed().toImmutableList(),
x = data.x,
y = data.y.reversed().toImmutableList(),
)
}
}
@ -383,9 +383,9 @@ private fun MarketChartPreview(
Button(
onClick = {
dataProducer.runTransaction {
updateLook {
it.copy(
type = when (it.type) {
updateLook { look ->
look.copy(
type = when (look.type) {
MarketChartLook.Type.Growing -> MarketChartLook.Type.Falling
MarketChartLook.Type.Falling -> MarketChartLook.Type.Neutral
MarketChartLook.Type.Neutral -> MarketChartLook.Type.Growing

View file

@ -104,10 +104,10 @@ private fun PreviewColumn() {
TangemThemePreview {
LazyColumn {
items(100) {
items(100) { i ->
MarketChartMini(
rawData = data,
type = if (it % 3 == 0) MarketChartLook.Type.Growing else MarketChartLook.Type.Falling,
type = if (i % 3 == 0) MarketChartLook.Type.Growing else MarketChartLook.Type.Falling,
)
}
}

View file

@ -51,10 +51,10 @@ object LTThreeBuckets {
val yRes = ArrayList<Double>(points.size)
val indexesRes = ArrayList<Int>(points.size)
results.fastForEach {
xRes.add(it.x)
yRes.add(it.y)
indexesRes.add(it.originalIndex!!)
results.fastForEach { result ->
xRes.add(result.x)
yRes.add(result.y)
indexesRes.add(requireNotNull(result.originalIndex))
}
return Result(

View file

@ -62,12 +62,7 @@ internal fun rememberTangemChartMarker(color: Color): CartesianMarker {
},
indicatorSizeDp = INDICATOR_SIZE_DP,
guideline = guideline,
valueFormatter = object : CartesianMarkerValueFormatter {
override fun format(
context: CartesianDrawContext,
targets: List<CartesianMarker.Target>,
): CharSequence = ""
},
valueFormatter = CartesianMarkerValueFormatter { _, _ -> "" },
) {
override fun updateInsets(
context: CartesianMeasureContext,
@ -76,7 +71,12 @@ internal fun rememberTangemChartMarker(color: Color): CartesianMarker {
insets: Insets,
) {
with(context) {
super.updateInsets(context, horizontalDimensions, model, insets)
super.updateInsets(
context = context,
horizontalDimensions = horizontalDimensions,
model = model,
insets = insets,
)
val baseShadowInsetDp =
CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER * LABEL_BACKGROUND_SHADOW_RADIUS_DP
val topInset = (baseShadowInsetDp - LABEL_BACKGROUND_SHADOW_DY_DP).pixels
@ -90,11 +90,11 @@ internal fun rememberTangemChartMarker(color: Color): CartesianMarker {
cacheStore
.getOrSet(keyNamespace, indicator, outColor) { indicator.invoke(outColor) }
.draw(
this,
x - halfIndicatorSize,
y - halfIndicatorSize,
x + halfIndicatorSize,
y + halfIndicatorSize,
context = this,
left = x - halfIndicatorSize,
top = y - halfIndicatorSize,
right = x + halfIndicatorSize,
bottom = y + halfIndicatorSize,
)
}
}

View file

@ -64,7 +64,7 @@ internal fun rememberMarketChartLayer(
val backgroundColorLineGradient = persistentListOf(backgroundLineColor, Color.Transparent)
val backgroundSecondLineColorGradient = persistentListOf(backgroundSecondLineColor, Color.Transparent)
val markerSet = markerFraction != null
val isMarkerSet = markerFraction != null
return rememberLayer(
fractionValue = markerFraction ?: 0f,
@ -75,17 +75,17 @@ internal fun rememberMarketChartLayer(
} else {
lineColor
},
backLineColor = if (markerSet && !secondColorOnTheRightSide) {
backLineColor = if (isMarkerSet && !secondColorOnTheRightSide) {
backgroundSecondLineColorGradient
} else {
backgroundColorLineGradient
},
lineColorRight = when {
markerSet && secondColorOnTheRightSide -> secondLineColor
isMarkerSet && secondColorOnTheRightSide -> secondLineColor
else -> lineColor
},
backLineColorRight = when {
markerSet && secondColorOnTheRightSide -> backgroundSecondLineColorGradient
isMarkerSet && secondColorOnTheRightSide -> backgroundSecondLineColorGradient
else -> backgroundColorLineGradient
},
)
@ -148,7 +148,7 @@ private fun LayerChartPreview(
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 {
val lineColor by remember {
mutableStateOf(Color.Blue)
}

View file

@ -85,7 +85,7 @@ class TransactionSuspend(
class MarketChartDataProducer private constructor(
initialData: MarketChartData,
initialLook: MarketChartLook,
val pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true),
val pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = true),
private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
) {
internal val dataState = MutableStateFlow(initialData)
@ -166,7 +166,7 @@ class MarketChartDataProducer private constructor(
* @return A MarketChartDataProducer.
*/
suspend fun buildSuspend(
pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true),
pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = true),
dispatcher: CoroutineDispatcher = Dispatchers.Default,
block: TransactionSuspend.() -> Unit,
): MarketChartDataProducer {
@ -191,7 +191,7 @@ class MarketChartDataProducer private constructor(
* @return A MarketChartDataProducer.
*/
fun build(
pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true),
pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = true),
dispatcher: CoroutineDispatcher = Dispatchers.Default,
block: Transaction.() -> Unit,
): MarketChartDataProducer {

View file

@ -9,14 +9,14 @@ import com.tangem.common.ui.charts.state.formatter.AxisLabelFormatter
* 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 shouldMarkerHighlightRightSide A boolean indicating whether the marker highlights the right side of the chart.
* @property xAxisFormatter A formatter for the x-axis labels.
* @property yAxisFormatter A formatter for the y-axis labels.
*/
@Immutable
data class MarketChartLook(
val type: Type = Type.Growing,
val markerHighlightRightSide: Boolean = true,
val shouldMarkerHighlightRightSide: Boolean = true,
val xAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() },
val yAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() },
) {

View file

@ -20,8 +20,8 @@ import java.math.BigDecimal
fun rememberMarketChartState(
dataProducer: MarketChartDataProducer = remember { MarketChartDataProducer.build {} },
colorMapper: (MarketChartLook.Type) -> Color = remember {
{
when (it) {
{ type ->
when (type) {
MarketChartLook.Type.Growing -> Color.Green
MarketChartLook.Type.Falling -> Color.Red
MarketChartLook.Type.Neutral -> Color.Gray
@ -33,7 +33,12 @@ fun rememberMarketChartState(
val lookState = dataProducer.lookState.collectAsState()
val state = remember(dataProducer, lookState, colorMapper, onMarkerShown) {
MarketChartState(dataProducer, lookState, colorMapper, onMarkerShown)
MarketChartState(
dataProducer = dataProducer,
lookState = lookState,
colorMapper = colorMapper,
markerCallback = onMarkerShown,
)
}
return state
@ -61,8 +66,8 @@ class MarketChartState internal constructor(
colorMapper(lookState.value.type)
}
internal val markerHighlightRightSide by derivedStateOf {
lookState.value.markerHighlightRightSide
internal val shouldMarkerHighlightRightSide by derivedStateOf {
lookState.value.shouldMarkerHighlightRightSide
}
internal val xValueFormatter = CartesianValueFormatter { value, _, _ ->

View file

@ -8,7 +8,7 @@ import java.math.BigDecimal
@Suppress("MagicNumber")
class PriceAndTimePointValuesConverter(
private val needToFormatAxis: Boolean,
private val shouldFormatAxis: Boolean,
) : PointValuesConverter {
private data class MinMaxCache(
@ -18,7 +18,12 @@ class PriceAndTimePointValuesConverter(
val maxY: BigDecimal,
)
private var minMaxCache = MinMaxCache(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO)
private var minMaxCache = MinMaxCache(
minX = BigDecimal.ZERO,
maxX = BigDecimal.ZERO,
minY = BigDecimal.ZERO,
maxY = BigDecimal.ZERO,
)
private val formatYValuesCache = mutableMapOf<Double, BigDecimal>()
private val formatXValuesCache = mutableMapOf<Double, BigDecimal>()
@ -37,15 +42,14 @@ class PriceAndTimePointValuesConverter(
val normX = data.x.normalizeTime(min = cache.minX, max = cache.maxX)
return if (normX.size > MAX_POINTS) {
LTThreeBuckets
val downsampled = LTThreeBuckets
.downsample(normX, normY, MAX_POINTS - 2)
.let {
MarketChartRawData(
originalIndexes = it.originalIndexes.toImmutableList(),
x = it.x.toImmutableList(),
y = it.y.toImmutableList(),
)
}
MarketChartRawData(
originalIndexes = downsampled.originalIndexes.toImmutableList(),
x = downsampled.x.toImmutableList(),
y = downsampled.y.toImmutableList(),
)
} else {
MarketChartRawData(
x = normX.toImmutableList(),
@ -55,8 +59,8 @@ class PriceAndTimePointValuesConverter(
}
override fun prepareRawXForFormat(rawX: Double, data: MarketChartData.Data): BigDecimal {
if (!needToFormatAxis) return BigDecimal.ZERO
if (formatXValuesCache.containsKey(rawX)) return formatXValuesCache[rawX]!!
if (!shouldFormatAxis) return BigDecimal.ZERO
if (formatXValuesCache.containsKey(rawX)) return requireNotNull(formatXValuesCache[rawX])
val result = (rawX * MINUTE).toBigDecimal()
@ -65,8 +69,8 @@ class PriceAndTimePointValuesConverter(
}
override fun prepareRawYForFormat(rawY: Double, data: MarketChartData.Data): BigDecimal {
if (!needToFormatAxis) return BigDecimal.ZERO
if (formatYValuesCache.containsKey(rawY)) return formatYValuesCache[rawY]!!
if (!shouldFormatAxis) return BigDecimal.ZERO
if (formatYValuesCache.containsKey(rawY)) return requireNotNull(formatYValuesCache[rawY])
val min = minMaxCache.minY
val max = minMaxCache.maxY