@@ -5,6 +5,7 @@ import androidx.compose.animation.core.AnimationVector1D
55import androidx.compose.animation.core.tween
66import androidx.compose.foundation.Canvas
77import androidx.compose.foundation.gestures.detectHorizontalDragGestures
8+ import androidx.compose.foundation.gestures.detectTapGestures
89import androidx.compose.foundation.layout.Arrangement
910import androidx.compose.foundation.layout.Column
1011import androidx.compose.foundation.layout.Row
@@ -34,6 +35,7 @@ import androidx.compose.ui.graphics.PathMeasure
3435import androidx.compose.ui.graphics.SolidColor
3536import androidx.compose.ui.graphics.drawscope.DrawScope
3637import androidx.compose.ui.graphics.drawscope.Stroke
38+ import androidx.compose.ui.input.pointer.PointerInputScope
3739import androidx.compose.ui.input.pointer.pointerInput
3840import androidx.compose.ui.platform.LocalDensity
3941import androidx.compose.ui.platform.LocalLayoutDirection
@@ -42,6 +44,7 @@ import androidx.compose.ui.text.TextStyle
4244import androidx.compose.ui.text.drawText
4345import androidx.compose.ui.text.rememberTextMeasurer
4446import androidx.compose.ui.unit.Dp
47+ import androidx.compose.ui.unit.IntSize
4548import androidx.compose.ui.unit.LayoutDirection
4649import androidx.compose.ui.unit.dp
4750import androidx.compose.ui.unit.sp
@@ -69,6 +72,7 @@ import ir.ehsannarmani.compose_charts.models.ZeroLineProperties
6972import ir.ehsannarmani.compose_charts.utils.HorizontalLabels
7073import ir.ehsannarmani.compose_charts.utils.calculateOffset
7174import kotlinx.coroutines.CoroutineScope
75+ import kotlinx.coroutines.Job
7276import kotlinx.coroutines.delay
7377import kotlinx.coroutines.launch
7478
@@ -149,7 +153,7 @@ fun LineChart(
149153 val linesPathData = remember {
150154 mutableStateListOf<PathData >()
151155 }
152- val indicators = remember(indicatorProperties.indicators,minValue,maxValue) {
156+ val indicators = remember(indicatorProperties.indicators, minValue, maxValue) {
153157 indicatorProperties.indicators.ifEmpty {
154158 split(
155159 count = indicatorProperties.count,
@@ -186,7 +190,7 @@ fun LineChart(
186190 launch {
187191 data.forEach {
188192 val animators = mutableListOf<Animatable <Float , AnimationVector1D >>()
189- it.values.forEach {
193+ repeat( it.values.size) {
190194 animators.add(Animatable (0f ))
191195 }
192196 dotAnimators.add(animators)
@@ -242,6 +246,101 @@ fun LineChart(
242246 linesPathData.clear()
243247 }
244248
249+ var tapJob: Job ? = null
250+ var isDisplayingPopup = false
251+ suspend fun hidePopup (
252+ withAnimation : Boolean = true
253+ ) {
254+ val duration = if (withAnimation) {
255+ 500
256+ } else {
257+ 0
258+ }
259+
260+ popupAnimation.animateTo(0f , animationSpec = tween(duration))
261+ popups.clear()
262+ popupsOffsetAnimators.clear()
263+ isDisplayingPopup = false
264+ }
265+
266+ fun PointerInputScope.showPopup (
267+ data : List <Line >,
268+ size : IntSize ,
269+ position : Offset
270+ ) {
271+ isDisplayingPopup = true
272+ popups.clear()
273+
274+ data.forEachIndexed { dataIndex, line ->
275+ val properties = line.popupProperties ? : popupProperties
276+ if (! properties.enabled) return @forEachIndexed
277+
278+ val positionX = position.x.coerceIn(0f , size.width.toFloat())
279+ val pathData = linesPathData[dataIndex]
280+
281+ if (positionX >= pathData.xPositions[pathData.startIndex] &&
282+ positionX <= pathData.xPositions[pathData.endIndex]
283+ ) {
284+ val showOnPointsThreshold =
285+ ((properties.mode as ? PopupProperties .Mode .PointMode )?.threshold
286+ ? : 0 .dp).toPx()
287+ val pointX = pathData.xPositions.find {
288+ it in positionX - showOnPointsThreshold.. positionX + showOnPointsThreshold
289+ }
290+
291+ if (properties.mode !is PopupProperties .Mode .PointMode || pointX != null ) {
292+ val relevantX =
293+ if (properties.mode is PopupProperties .Mode .PointMode ) (pointX?.toFloat()
294+ ? : 0f ) else positionX
295+ val fraction = (relevantX / size.width)
296+
297+ val valueIndex = calculateValueIndex(
298+ fraction = fraction.toDouble(),
299+ values = line.values,
300+ pathData = pathData
301+ )
302+
303+ val popupValue = getPopupValue(
304+ points = line.values,
305+ fraction = fraction.toDouble(),
306+ rounded = line.curvedEdges ? : curvedEdges,
307+ size = size.toSize(),
308+ minValue = minValue,
309+ maxValue = maxValue
310+ )
311+
312+ popups.add(
313+ Popup (
314+ position = popupValue.offset,
315+ value = popupValue.calculatedValue,
316+ properties = properties,
317+ dataIndex = dataIndex,
318+ valueIndex = valueIndex
319+ )
320+ )
321+
322+ if (popupsOffsetAnimators.count() < popups.count()) {
323+ repeat(popups.count() - popupsOffsetAnimators.count()) {
324+ popupsOffsetAnimators.add(
325+ if (properties.mode is PopupProperties .Mode .PointMode ) {
326+ Animatable (popupValue.offset.x) to Animatable (popupValue.offset.y)
327+ } else {
328+ Animatable (0f ) to Animatable (0f )
329+ }
330+ )
331+ }
332+ }
333+ }
334+ }
335+ }
336+
337+ scope.launch {
338+ if (popupAnimation.value != 1f && ! popupAnimation.isRunning) {
339+ popupAnimation.animateTo(1f , animationSpec = tween(500 ))
340+ }
341+ }
342+ }
343+
245344 Column (modifier = modifier) {
246345 if (labelHelperProperties.enabled) {
247346 LabelHelper (
@@ -262,97 +361,59 @@ fun LineChart(
262361 }
263362 }
264363 CompositionLocalProvider (LocalLayoutDirection provides LayoutDirection .Ltr ) {
265- Canvas (modifier = Modifier
266- .weight(1f )
267- .fillMaxSize()
268- .pointerInput(data, minValue, maxValue, linesPathData) {
269- if (! popupProperties.enabled || data.all { it.popupProperties?.enabled == false }) return @pointerInput
270- detectHorizontalDragGestures(
271- onDragEnd = {
272- scope.launch {
273- popupAnimation.animateTo(0f , animationSpec = tween(500 ))
274- popups.clear()
275- popupsOffsetAnimators.clear()
364+ Canvas (
365+ modifier = Modifier
366+ .weight(1f )
367+ .fillMaxSize()
368+ .pointerInput(data, minValue, maxValue, linesPathData) {
369+ if (! popupProperties.enabled || data.all { it.popupProperties?.enabled == false })
370+ return @pointerInput
371+
372+ detectHorizontalDragGestures(
373+ onDragEnd = {
374+ scope.launch {
375+ hidePopup()
376+ }
377+ },
378+ onHorizontalDrag = { change, amount ->
379+ showPopup(
380+ data = data,
381+ size = size,
382+ position = change.position
383+ )
276384 }
277- },
278- onHorizontalDrag = { change, amount ->
279- val _size = size.toSize()
280- .copy(height = (size.height).toFloat())
281- popups.clear()
282- data.forEachIndexed { dataIndex, line ->
283- val properties = line.popupProperties ? : popupProperties
284- if (properties.enabled){
285- val positionX =
286- (change.position.x).coerceIn(
287- 0f ,
288- size.width.toFloat()
289- )
290- val pathData = linesPathData[dataIndex]
291-
292- if (positionX >= pathData.xPositions[pathData.startIndex] && positionX <= pathData.xPositions[pathData.endIndex]) {
293- val showOnPointsThreshold =
294- ((properties.mode as ? PopupProperties .Mode .PointMode )?.threshold
295- ? : 0 .dp).toPx()
296- val pointX =
297- pathData.xPositions.find { it in positionX - showOnPointsThreshold.. positionX + showOnPointsThreshold }
298-
299- if (properties.mode !is PopupProperties .Mode .PointMode || pointX != null ) {
300- val fraction =
301- ((if (properties.mode is PopupProperties .Mode .PointMode ) (pointX?.toFloat()
302- ? : 0f ) else positionX) / size.width)
303-
304- // Calculate the data index
305- val valueIndex = calculateValueIndex(
306- fraction = fraction.toDouble(),
307- values = line.values,
308- pathData = pathData
309- )
310-
311- val popupValue = getPopupValue(
312- points = line.values,
313- fraction = fraction.toDouble(),
314- rounded = line.curvedEdges ? : curvedEdges,
315- size = _size ,
316- minValue = minValue,
317- maxValue = maxValue
318- )
319- popups.add(
320- Popup (
321- position = popupValue.offset,
322- value = popupValue.calculatedValue,
323- properties = properties,
324- dataIndex = dataIndex,
325- valueIndex = valueIndex
326- )
327- )
328- if (popupsOffsetAnimators.count() < popups.count()) {
329- repeat(popups.count() - popupsOffsetAnimators.count()) {
330- popupsOffsetAnimators.add(
331- // add fixed position for popup when mode is point mode
332- if (properties.mode is PopupProperties .Mode .PointMode ) {
333- Animatable (popupValue.offset.x) to Animatable (
334- popupValue.offset.y
335- )
336- } else {
337- Animatable (0f ) to Animatable (0f )
338- }
339- )
340- }
341- }
342- }
343- }
385+ )
386+ }
387+ .pointerInput(Unit ) {
388+ if (! popupProperties.enabled || data.all { it.popupProperties?.enabled == false })
389+ return @pointerInput
390+
391+ detectTapGestures(
392+ onTap = {
393+ if (tapJob != null && tapJob?.isActive == true ) {
394+ tapJob?.cancel()
395+ tapJob = null
344396 }
345397
346- }
347- scope.launch {
348- // animate popup (alpha)
349- if (popupAnimation.value != 1f && ! popupAnimation.isRunning) {
350- popupAnimation.animateTo(1f , animationSpec = tween(500 ))
398+ tapJob = scope.launch {
399+ if (isDisplayingPopup) {
400+ hidePopup(withAnimation = false )
401+ }
402+
403+ showPopup(
404+ data = data,
405+ size = size,
406+ position = it
407+ )
408+
409+ delay(500 )
410+ if (isDisplayingPopup) {
411+ hidePopup(withAnimation = true )
412+ }
351413 }
352- }
353- }
354- )
355- }
414+ },
415+ )
416+ }
356417 ) {
357418 val chartAreaHeight = size.height
358419 chartWidth.value = size.width
@@ -373,9 +434,12 @@ fun LineChart(
373434 }
374435 if (linesPathData.isEmpty() || linesPathData.count() != data.count()) {
375436 data.map {
376- val startIndex = if (it.viewRange.startIndex < 0 || it.viewRange.startIndex >= it.values.size - 1 ) 0 else it.viewRange.startIndex
377- val endIndex = if (it.viewRange.endIndex < 0 || it.viewRange.endIndex <= it.viewRange.startIndex
378- || it.viewRange.endIndex > it.values.size - 1 ) it.values.size - 1 else it.viewRange.endIndex
437+ val startIndex =
438+ if (it.viewRange.startIndex < 0 || it.viewRange.startIndex >= it.values.size - 1 ) 0 else it.viewRange.startIndex
439+ val endIndex =
440+ if (it.viewRange.endIndex < 0 || it.viewRange.endIndex <= it.viewRange.startIndex
441+ || it.viewRange.endIndex > it.values.size - 1
442+ ) it.values.size - 1 else it.viewRange.endIndex
379443
380444 getLinePath(
381445 dataPoints = it.values.map { it.toFloat() },
@@ -430,11 +494,11 @@ fun LineChart(
430494
431495 var startOffset = 0f
432496 var endOffset = size.width
433- if (pathData.startIndex > 0 ) {
434- startOffset = pathData.xPositions[pathData.startIndex] .toFloat()
497+ if (pathData.startIndex > 0 ) {
498+ startOffset = pathData.xPositions[pathData.startIndex].toFloat()
435499 }
436500
437- if (pathData.endIndex < line.values.size - 1 ) {
501+ if (pathData.endIndex < line.values.size - 1 ) {
438502 endOffset = pathData.xPositions[pathData.endIndex].toFloat()
439503 }
440504
@@ -521,11 +585,10 @@ fun LineChart(
521585}
522586
523587
524-
525588@Composable
526589private fun Indicators (
527590 modifier : Modifier = Modifier ,
528- indicators : List <Double >,
591+ indicators : List <Double >,
529592 indicatorProperties : HorizontalIndicatorProperties ,
530593) {
531594 Column (
@@ -676,7 +739,7 @@ fun DrawScope.drawDots(
676739 pathMeasure.setPath(linePath, false )
677740 val lastPosition = pathMeasure.getPosition(pathMeasure.length)
678741 dataPoints.forEachIndexed { valueIndex, value ->
679- if (valueIndex in startIndex.. endIndex) {
742+ if (valueIndex in startIndex.. endIndex) {
680743 val dotOffset = Offset (
681744 x = _size .width.spaceBetween(
682745 itemCount = dataPoints.count(),
0 commit comments