From 87e8e28ab7352733e478220906efbb9d32c0352b Mon Sep 17 00:00:00 2001 From: Saad Khan Date: Sat, 6 Jun 2026 22:36:03 +0500 Subject: [PATCH 1/2] fix(LineChart): correct PointMode valueIndex with nearest data point Replace indexOfFirst lookup that biased toward the next index due to floating-point rounding. Find the nearest xPositions entry within the PointMode threshold and snap popups to the exact data point value. --- .../ehsannarmani/compose_charts/LineChart.kt | 87 ++++++++++++++----- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/compose-charts/src/commonMain/kotlin/ir/ehsannarmani/compose_charts/LineChart.kt b/compose-charts/src/commonMain/kotlin/ir/ehsannarmani/compose_charts/LineChart.kt index ee478c2..2c6e3e7 100644 --- a/compose-charts/src/commonMain/kotlin/ir/ehsannarmani/compose_charts/LineChart.kt +++ b/compose-charts/src/commonMain/kotlin/ir/ehsannarmani/compose_charts/LineChart.kt @@ -81,6 +81,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlin.math.abs private data class Popup( val properties: PopupProperties, @@ -277,32 +278,42 @@ fun LineChart( val showOnPointsThreshold = ((properties.mode as? PopupProperties.Mode.PointMode)?.threshold ?: 0.dp).toPx() - val pointX = pathData.xPositions.find { - it in positionX - showOnPointsThreshold..positionX + showOnPointsThreshold + val pointIndex = if (properties.mode is PopupProperties.Mode.PointMode && !isSingleValue) { + findNearestValueIndex( + x = positionX, + pathData = pathData, + valuesCount = line.values.size, + threshold = showOnPointsThreshold + ) + } else { + null } - if (properties.mode !is PopupProperties.Mode.PointMode || pointX != null || isSingleValue) { - val relevantX = - if (properties.mode is PopupProperties.Mode.PointMode) (pointX?.toFloat() - ?: 0f) else positionX - val fraction = (relevantX / size.width) + if (properties.mode !is PopupProperties.Mode.PointMode || pointIndex != null || isSingleValue) { + val relevantX = when { + isSingleValue -> 0f + pointIndex != null -> pathData.xPositions[pointIndex].toFloat() + else -> positionX + } + val fraction = relevantX / size.width - val valueIndex = if(isSingleValue){ - 0 - }else{ - calculateValueIndex( + val valueIndex = when { + isSingleValue -> 0 + pointIndex != null -> pointIndex + else -> calculateValueIndex( fraction = fraction.toDouble(), values = line.values, - pathData = pathData + pathData = pathData, + chartWidth = size.width.toFloat() ) } - val popupValue = if(isSingleValue){ + val popupValue = if (isSingleValue) { Value( calculatedValue = line.values.first(), offset = Offset( x = 0f, - y = size.height-calculateOffset( + y = size.height - calculateOffset( maxValue = maxValue, minValue = minValue, value = line.values.first().toFloat(), @@ -310,7 +321,21 @@ fun LineChart( ).toFloat() ) ) - }else{ + } else if (pointIndex != null && properties.mode is PopupProperties.Mode.PointMode) { + val value = line.values[pointIndex] + Value( + calculatedValue = value, + offset = Offset( + x = pathData.xPositions[pointIndex].toFloat(), + y = size.height - calculateOffset( + maxValue = computedMaxValue, + minValue = minValue, + total = size.height.toFloat(), + value = value.toFloat() + ).toFloat() + ) + ) + } else { getPopupValue( points = line.values, fraction = fraction.toDouble(), @@ -622,16 +647,36 @@ private fun Indicators( } } +private fun findNearestValueIndex( + x: Float, + pathData: PathData, + valuesCount: Int, + threshold: Float? = null, +): Int? { + if (valuesCount <= 0) return null + val lastIndex = valuesCount - 1 + val nearestIndex = pathData.xPositions.indices + .filter { it <= lastIndex } + .minByOrNull { abs(pathData.xPositions[it] - x) } + ?: return null + if (threshold != null && abs(pathData.xPositions[nearestIndex] - x) > threshold) { + return null + } + return nearestIndex +} + private fun calculateValueIndex( fraction: Double, values: List, - pathData: PathData + pathData: PathData, + chartWidth: Float, ): Int { - val xPosition = (fraction * pathData.path.getBounds().width).toFloat() - val closestXIndex = pathData.xPositions.indexOfFirst { x -> - x >= xPosition - } - return if (closestXIndex >= 0) closestXIndex else values.size - 1 + if (values.size <= 1) return 0 + return findNearestValueIndex( + x = (fraction * chartWidth).toFloat(), + pathData = pathData, + valuesCount = values.size + ) ?: values.lastIndex } private fun DrawScope.drawPopup( From 4c0ed35cf294e127447aa758fce01137bd571a08 Mon Sep 17 00:00:00 2001 From: Saad Khan Date: Sat, 6 Jun 2026 22:36:11 +0500 Subject: [PATCH 2/2] fix(LineChart): align dot x positions with line path xPositions Use the same x coordinates as the line path when drawing dots so marker positions stay consistent with large datasets and view ranges. --- .../kotlin/ir/ehsannarmani/compose_charts/LineChart.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/compose-charts/src/commonMain/kotlin/ir/ehsannarmani/compose_charts/LineChart.kt b/compose-charts/src/commonMain/kotlin/ir/ehsannarmani/compose_charts/LineChart.kt index 2c6e3e7..9027b35 100644 --- a/compose-charts/src/commonMain/kotlin/ir/ehsannarmani/compose_charts/LineChart.kt +++ b/compose-charts/src/commonMain/kotlin/ir/ehsannarmani/compose_charts/LineChart.kt @@ -584,6 +584,7 @@ fun LineChart( pathMeasure = pathMeasure, scope = scope, size = size, + xPositions = pathData.xPositions, startIndex = pathData.startIndex, endIndex = pathData.endIndex ) @@ -798,6 +799,7 @@ private fun DrawScope.drawDots( pathMeasure: PathMeasure, scope: CoroutineScope, size: Size? = null, + xPositions: List, startIndex: Int, endIndex: Int, ) { @@ -812,11 +814,11 @@ private fun DrawScope.drawDots( properties.confirmDraw(DotProperties.Dot(value.dataIndex, valueIndex, value.value.toDouble())) && valueIndex in startIndex..endIndex ) { + val dotX = xPositions.getOrElse(valueIndex) { + if (dataPoints.size <= 1) 0.0 else valueIndex * _size.width.toDouble() / (dataPoints.size - 1) + }.toFloat() val dotOffset = Offset( - x = _size.width.spaceBetween( - itemCount = dataPoints.count(), - index = valueIndex - ), + x = dotX, y = (_size.height - calculateOffset( maxValue = maxValue.toDouble(), minValue = minValue.toDouble(),