KoalaPlot / koalaplot-core

Koala Plot is a Compose Multiplatform based charting and plotting library written in Kotlin
https://koalaplot.github.io/
MIT License
382 stars 18 forks source link

Crash when mutating data in PieChart #60

Open sadellie opened 5 months ago

sadellie commented 5 months ago

When rapidly changing chart data size, the app crashes. I think generateHueColorPalette gets called too late.

Sample

Spam-click Toggle list button to reproduce:

fun main() {
    application {
        Window(
            onCloseRequest = this::exitApplication,
            title = "Demo app"
        ) {
            Column {
                var showBigList by remember { mutableStateOf(false) }

                val data = remember(showBigList) {
                    List(size = if (showBigList) 100 else 5) { it.toFloat() }
                }

                Button(
                    onClick = { showBigList = !showBigList }
                ) {
                    Text("Toggle list")
                }

                PieChart(
                    values = data
                )
            }
        }
    }
}

Logs:

Exception in thread "AWT-EventQueue-0" java.lang.IndexOutOfBoundsException: index: 5, size: 5
    at kotlin.collections.AbstractList$Companion.checkElementIndex$kotlin_stdlib(AbstractList.kt:108)
    at kotlin.collections.builders.ListBuilder.get(ListBuilder.kt:39)
    at io.github.koalaplot.core.pie.PieChartKt$PieChart$7.invoke(PieChart.kt:339)
    at io.github.koalaplot.core.pie.PieChartKt$PieChart$7.invoke(PieChart.kt:337)
    at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jb.kt:137)
    at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jb.kt:33)
    at io.github.koalaplot.core.pie.PieChartKt$Pie$1$1.invoke(PieChart.kt:397)
    at io.github.koalaplot.core.pie.PieChartKt$Pie$1$1.invoke(PieChart.kt:390)
    at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jb.kt:116)
    at androidx.compose.runtime.internal.ComposableLambdaImpl$invoke$2.invoke(ComposableLambda.jb.kt:128)
    at androidx.compose.runtime.internal.ComposableLambdaImpl$invoke$2.invoke(ComposableLambda.jb.kt:127)
    at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:192)
    at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2557)
    at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2828)
    at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3315)
    at androidx.compose.runtime.ComposerImpl.recompose$runtime(Composer.kt:3266)
    at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:940)
    at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:1155)
    at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:127)
    at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:583)
    at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:551)
    at androidx.compose.runtime.BroadcastFrameClock$FrameAwaiter.resume(BroadcastFrameClock.kt:42)
    at androidx.compose.runtime.BroadcastFrameClock.sendFrame(BroadcastFrameClock.kt:71)
    at androidx.compose.ui.scene.BaseComposeScene.render(BaseComposeScene.skiko.kt:163)
    at androidx.compose.ui.scene.ComposeSceneMediator$DesktopSkikoView.onRender(ComposeSceneMediator.desktop.kt:523)
    at org.jetbrains.skiko.SkiaLayer.update$skiko(SkiaLayer.awt.kt:548)
    at org.jetbrains.skiko.redrawer.AWTRedrawer.update(AWTRedrawer.kt:54)
    at org.jetbrains.skiko.redrawer.Direct3DRedrawer$frameDispatcher$1.invokeSuspend(Direct3DRedrawer.kt:49)
    at org.jetbrains.skiko.redrawer.Direct3DRedrawer$frameDispatcher$1.invoke(Direct3DRedrawer.kt)
    at org.jetbrains.skiko.redrawer.Direct3DRedrawer$frameDispatcher$1.invoke(Direct3DRedrawer.kt)
    at org.jetbrains.skiko.FrameDispatcher$job$1.invokeSuspend(FrameDispatcher.kt:33)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
    at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:87)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
    Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.ui.scene.ComposeContainer$DesktopCoroutineExceptionHandler@2b52488f, androidx.compose.runtime.BroadcastFrameClock@5bc6f3a2, StandaloneCoroutine{Cancelling}@7d783f0a, FlushCoroutineDispatcher@635857e4]

Info

I managed to fix this by adding nullability when getting slice color. I found no issues with this workaround:

fun main() {
    application {
        Window(
            onCloseRequest = this::exitApplication,
            title = "Demo app"
        ) {
            Column {
                var showBigList by remember { mutableStateOf(false) }

                val data = remember(showBigList) {
                    List(size = if (showBigList) 100 else 5) { it.toFloat() }
                }

                Button(
                    onClick = { showBigList = !showBigList }
                ) {
                    Text("Toggle list")
                }

                PieChart(
                    values = data
                    slice = {
                        val colors = remember(data.size) { generateHueColorPalette(data.size) }
                        DefaultSlice(colors.getOrNull(it) ?: Color.Transparent) // Can be null here
                    }
                )
            }
        }
    }
}

Library version: 0.6.0 Compose: 1.6.2 (Multiplatform) Platform: Windows 10

gsteckman commented 4 months ago

I've tried this with Compose 1.6.0, 1.6.2, and 1.6.10, also on Windows 10, and can't repeat it. I think remember() should return the result of the calculation before proceeding, and this looks like a race condition. I don't know what's causing this. Can you try val colors = generateHueColorPalette(data.size), without the remember, to see what happens?

sadellie commented 4 months ago

Still crashes.

fun main() {
    application {
        Window(
            onCloseRequest = this::exitApplication,
            title = "Demo app"
        ) {
            Column {
                var showBigList by remember { mutableStateOf(false) }

                val data = remember(showBigList) {
                    List(size = if (showBigList) 100 else 5) { it.toFloat() }
                }

                Button(
                    onClick = { showBigList = !showBigList }
                ) {
                    Text("Toggle list")
                }

                PieChart(
                    values = data,
                    slice = {
                        val colors = generateHueColorPalette(data.size)
                        DefaultSlice(colors[it])
                    }
                )
            }
        }
    }
}

I also tried to move colors out of slice. But no success.

fun main() {
    application {
        Window(
            onCloseRequest = this::exitApplication,
            title = "Demo app"
        ) {
            Column {
                var showBigList by remember { mutableStateOf(false) }

                val data = remember(showBigList) {
                    List(size = if (showBigList) 100 else 5) { it.toFloat() }
                }
                val colors = generateHueColorPalette(data.size) // Moved here

                Button(
                    onClick = { showBigList = !showBigList }
                ) {
                    Text("Toggle list")
                }

                PieChart(
                    values = data,
                    slice = {

                        DefaultSlice(colors[it])
                    }
                )
            }
        }
    }
}

I am still using my workaround which is not ideal since there are few frames where slices are Color.Transparent. They are not noticeable due to pie animation.

gsteckman commented 4 months ago

If I can get it to happen, maybe I can find the reason. How frequently are you pushing the Toggle List button?

sadellie commented 4 months ago

Very frequently. Like, a double click type of frequently

gsteckman commented 2 months ago

The cause of this might be related to #71.