patrykandpatrick / vico

A light and extensible chart library for Android.
https://patrykandpatrick.com/vico/wiki
Apache License 2.0
2.15k stars 130 forks source link

Crash in ScrollHandler when number of entries is one #126

Closed Samuel-Ambrosio closed 2 years ago

Samuel-Ambrosio commented 2 years ago

Hello! With the last version 1.4.2, when the number of entries for the chart is one and then the chart is moved, the application crashes.

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: app.package.custom, PID: 17717
    java.lang.IllegalArgumentException: Cannot coerce value to an empty range: NaN..0.0.
        at kotlin.ranges.RangesKt___RangesKt.coerceIn(_Ranges.kt:1292)
        at com.patrykandpatryk.vico.core.scroll.ScrollHandler.getClampedScroll(ScrollHandler.kt:40)
        at com.patrykandpatryk.vico.core.scroll.ScrollHandler.handleScrollDelta(ScrollHandler.kt:48)
        at com.patrykandpatryk.vico.compose.chart.ChartsKt$Chart$scrollableState$1.invoke(Charts.kt:190)
        at com.patrykandpatryk.vico.compose.chart.ChartsKt$Chart$scrollableState$1.invoke(Charts.kt:190)
        at androidx.compose.foundation.gestures.ScrollableStateKt$rememberScrollableState$1$1.invoke(ScrollableState.kt:118)
        at androidx.compose.foundation.gestures.ScrollableStateKt$rememberScrollableState$1$1.invoke(ScrollableState.kt:118)
        at androidx.compose.foundation.gestures.DefaultScrollableState$scrollScope$1.scrollBy(ScrollableState.kt:136)
        at androidx.compose.foundation.gestures.ScrollingLogic.dispatchScroll-f0eR0lY(Scrollable.kt:350)
        at androidx.compose.foundation.gestures.ScrollDraggableState.dragBy-Uv8p0NA(Scrollable.kt:434)
        at androidx.compose.foundation.gestures.DraggableKt$draggable$9$2$2.invokeSuspend(Draggable.kt:244)
        at androidx.compose.foundation.gestures.DraggableKt$draggable$9$2$2.invoke(Unknown Source:8)
        at androidx.compose.foundation.gestures.DraggableKt$draggable$9$2$2.invoke(Unknown Source:4)
        at androidx.compose.foundation.gestures.ScrollDraggableState$drag$2.invokeSuspend(Scrollable.kt:445)
        at androidx.compose.foundation.gestures.ScrollDraggableState$drag$2.invoke(Unknown Source:8)
        at androidx.compose.foundation.gestures.ScrollDraggableState$drag$2.invoke(Unknown Source:4)
        at androidx.compose.foundation.gestures.DefaultScrollableState$scroll$2$1.invokeSuspend(ScrollableState.kt:150)
        at androidx.compose.foundation.gestures.DefaultScrollableState$scroll$2$1.invoke(Unknown Source:8)
        at androidx.compose.foundation.gestures.DefaultScrollableState$scroll$2$1.invoke(Unknown Source:4)
        at androidx.compose.foundation.MutatorMutex$mutateWith$2.invokeSuspend(MutatorMutex.kt:160)
        at androidx.compose.foundation.MutatorMutex$mutateWith$2.invoke(Unknown Source:8)
        at androidx.compose.foundation.MutatorMutex$mutateWith$2.invoke(Unknown Source:4)
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
        at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:264)
        at androidx.compose.foundation.MutatorMutex.mutateWith(MutatorMutex.kt:153)
        at androidx.compose.foundation.gestures.DefaultScrollableState$scroll$2.invokeSuspend(ScrollableState.kt:147)
        at androidx.compose.foundation.gestures.DefaultScrollableState$scroll$2.invoke(Unknown Source:8)
        at androidx.compose.foundation.gestures.DefaultScrollableState$scroll$2.invoke(Unknown Source:4)
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
        at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:264)
        at androidx.compose.foundation.gestures.DefaultScrollableState.scroll(ScrollableState.kt:146)
        at androidx.compose.foundation.gestures.ScrollDraggableState.drag(Scrollable.kt:443)
        at androidx.compose.foundation.gestures.DraggableKt$draggable$9$2.invokeSuspend(Draggable.kt:241)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch(AndroidUiDispatcher.android.kt:81)
        at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch(AndroidUiDispatcher.android.kt:41)
        at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:68)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:970)
        at android.view.Choreographer.doCallbacks(Choreographer.java:796)
        at android.view.Choreographer.doFrame(Choreographer.java:727)
E/AndroidRuntime:     at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
        Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [androidx.compose.ui.platform.MotionDurationScaleImpl@b01904e, androidx.compose.runtime.BroadcastFrameClock@3f6656f, StandaloneCoroutine{Cancelling}@5ab327c, AndroidUiDispatcher@b2e7705]
I/Process: Sending signal. PID: 17717 SIG: 9

In version 1.3.0 the application does not crash. And in both versions, the value is not shown in the chart and the axis has wrong values. (The value not showing up is the same in a columnChart)

Version 1.3.0: image

Version 1.4.2: image

Video from version 1.4.2:

https://user-images.githubusercontent.com/44056254/185653497-7afeafa3-1bd8-4731-a9cc-ef1a243f25c7.mp4

Code for chart:

val chartModelProducer = remember { ChartEntryModelProducer() }

val (entries, localDateTimes) =
    remember(
        marketPrices,
        marketPricesFilter,
        marketPricesTypeFilter,
        filterValues,
        groupByDay
    ) {
        marketPrices.mapToChartData(
            filterMarketPricesPredicate = { it.type == marketPricesTypeFilter },
            filterMarketPricesValuesPredicate = filterValues,
            marketPricesFilter = marketPricesFilter,
            groupByDay = groupByDay
        ).let { data ->
            Pair(data.map { it.second }, data.map { it.first })
        }
    }

LaunchedEffect(entries) {
    chartModelProducer.setEntries(entries)
}

ProvideChartStyle(
    chartStyle =
        m3ChartStyle(
            axisLabelColor = colors.onSurface,
            entityColors = listOf(colors.primary, colors.secondary),
            elevationOverlayColor = colors.primary)
) {
    Chart(
        chart =
            lineChart(
                minY = entries.takeIf { it.size > 1 }?.minOfOrNull { it.y },
                maxY = entries.takeIf { it.size > 1 }?.maxOfOrNull { it.y },
                persistentMarkers =
                    if (showCurrentHourMarker && entries.size > 1)
                        getCurrentHourMarker(entries = entries, localDateTimes = localDateTimes)
                    else null
            ),
        chartModelProducer = chartModelProducer,
        modifier = modifier,
        startAxis = startAxis(
            title = stringResource(id = R.string.euro_per_megawatt_hour),
            titleComponent = textComponent {},
            valueFormatter = EmptyAxisValueFormatter()),
        endAxis = endAxis(valueFormatter = MarketPricesAxisValueFormatter()),
        bottomAxis =
            bottomAxis(
                title = stringResource(id = if (groupByDay) R.string.time_unit_day else R.string.time_unit_hour),
                titleComponent = textComponent {},
                valueFormatter =
                    LocalDateTimeAxisValueFormatter(
                        localDateTimes = localDateTimes,
                        groupByDay = groupByDay)),
        marker = marker(),
        isZoomEnabled = isZoomEnabled)
}

Code for value formatter:

class LocalDateTimeAxisValueFormatter<Position: AxisPosition>(
      localDateTimes: List<LocalDateTime>,
      groupByDay: Boolean
): LocalDateTimeValueFormatter(localDateTimes, groupByDay), AxisValueFormatter<Position>

open class LocalDateTimeValueFormatter(
    private val localDateTimes: List<LocalDateTime>,
    private val groupByDay: Boolean,
): ValueFormatter {
    override fun formatValue(value: Float, chartValues: ChartValues): CharSequence {
        val dateFormatted =
            localDateTimes.getOrNull(value.toInt())?.format(
                if (groupByDay) DateFormat.DAY_MONTH else DateFormat.HOUR
            ) ?: ""
        return if (dateFormatted.isNotBlank()) (dateFormatted + (if (groupByDay) "" else "h")) else ""
    }
}

Example of data:

val entries = mutableListOf<FloatEntry>()
for (i in 0 until 24) entries.add(FloatEntry(i.toFloat(), Random.nextFloat()*(50) + 200))
val localDateTimes = mutableListOf<LocalDateTime>()
for (i in 0 until 24) dates.add(LocalDateTime.now().withHour(i).withMinute(0))

Thanks for the library, it's a great help! ❤️

ghost commented 2 years ago

Hello! We’ve recently released Vico 1.4.3, and charts containing only one entry should now behave as expected. Please keep in mind that in the case of line charts, at least two entries are required for a line to be drawn. A single-entry line chart with the point field set to null is empty. Should you face any further issues, please don’t hesitate to let us know. Thanks for the bug report!