dautovicharis / Charts

📈 Charts made with Jetpack Compose – Multiplatform support: 📱🌐💻
https://dautovicharis.github.io/Charts/2.0.0-SNAPSHOT/
MIT License
185 stars 12 forks source link

When I update `items` in `BarChartView` with number of entries larger then before (e.g. from 7 to 24) I get `IndexOutOfBoundsException` #140

Open dautovicharis opened 3 weeks ago

dautovicharis commented 3 weeks ago
          > Please check the readme on how to use snapshot releases. If you have any questions let me know.

Thanks for the explanation. I got 2.0.0-SNAPSHOT working without any issues.

Well, some issues, but they were also present in 1.3.1. When I update items in BarChartView with number of entries larger then before (e.g. from 7 to 24) I get IndexOutOfBoundsException

Originally posted by @pacjo in https://github.com/dautovicharis/Charts/issues/97#issuecomment-2295335785

pacjo commented 3 weeks ago

Ok, yeah, it should have been a separate issue.

As I edited the comment, crash appears when number of entries changes in any way (not only increases) (in the logs below going from 30 to 7).

logs ``` FATAL EXCEPTION: main Process: nodomain.pacjo.healthconnect.viewer, PID: 4274 java.lang.IndexOutOfBoundsException: Index 30 out of bounds for length 30 at jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64) at jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70) at jdk.internal.util.Preconditions.checkIndex(Preconditions.java:266) at java.util.Objects.checkIndex(Objects.java:359) at java.util.ArrayList.get(ArrayList.java:434) at io.github.dautovicharis.charts.internal.barchart.BarChartKt.drawBars-ks2KLzI(BarChart.kt:106) at io.github.dautovicharis.charts.internal.barchart.BarChartKt.access$drawBars-ks2KLzI(BarChart.kt:1) at io.github.dautovicharis.charts.internal.barchart.BarChartKt$BarChart$4.invoke(BarChart.kt:74) at io.github.dautovicharis.charts.internal.barchart.BarChartKt$BarChart$4.invoke(BarChart.kt:54) at androidx.compose.ui.draw.DrawBackgroundModifier.draw(DrawModifier.kt:116) at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105) at androidx.compose.ui.node.LayoutNodeDrawScope.draw-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:86) at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:364) at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176) at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361) at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176) at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361) at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176) at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361) at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353) at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:926) at androidx.compose.ui.node.InnerNodeCoordinator.performDraw(InnerNodeCoordinator.kt:174) at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361) at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176) at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361) at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353) at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:926) at androidx.compose.ui.node.InnerNodeCoordinator.performDraw(InnerNodeCoordinator.kt:174) at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361) at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353) at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176) at androidx.compose.ui.node.LayoutNodeDrawScope.drawContent(LayoutNodeDrawScope.kt:66) at androidx.compose.foundation.BackgroundNode.draw(Background.kt:159) at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105) at androidx.compose.ui.node.LayoutNodeDrawScope.draw-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:86) at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:364) at androidx.compose.ui.node.NodeCoordinator.access$drawContainedDrawModifiers(NodeCoordinator.kt:54) at androidx.compose.ui.node.NodeCoordinator$drawBlock$1$1.invoke(NodeCoordinator.kt:383) at androidx.compose.ui.node.NodeCoordinator$drawBlock$1$1.invoke(NodeCoordinator.kt:382) at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2303) at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:500) at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:256) at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:133) at androidx.compose.ui.node.NodeCoordinator$drawBlock$1.invoke(NodeCoordinator.kt:382) at androidx.compose.ui.node.NodeCoordinator$drawBlock$1.invoke(NodeCoordinator.kt:380) at androidx.compose.ui.platform.RenderNodeApi29.record(RenderNodeApi29.android.kt:209) at androidx.compose.ui.platform.RenderNodeLayer.updateDisplayList(RenderNodeLayer.android.kt:335) at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:1236) at android.view.View.draw(View.java:24630) at android.view.View.updateDisplayListIfDirty(View.java:23493) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4566) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4539) at android.view.View.updateDisplayListIfDirty(View.java:23449) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4566) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4539) at android.view.View.updateDisplayListIfDirty(View.java:23449) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4566) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4539) at android.view.View.updateDisplayListIfDirty(View.java:23449) at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4566) at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4539) at android.view.View.updateDisplayListIfDirty(View.java:23449) at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:694) at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:700) at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:798) at android.view.ViewRootImpl.draw(ViewRootImpl.java:5313) at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:4975) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:4093) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2718) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9941) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1406) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1415) at android.view.Choreographer.doCallbacks(Choreographer.java:1015) at android.view.Choreographer.doFrame(Choreographer.java:945) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1389) at android.os.Handler.handleCallback(Handler.java:959) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loopOnce(Looper.java:232) at android.os.Looper.loop(Looper.java:317) at android.app.ActivityThread.main(ActivityThread.java:8594) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:583) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878) ```
code causing the issue (a bit of a mess since I'm just starting) ```kotlin package nodomain.pacjo.healthconnect.viewer import [...] class MainActivity : ComponentActivity() { // TODO: move this out too private val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract() private val PERMISSIONS = setOf( HealthPermission.getReadPermission(StepsRecord::class), // TODO: add the rest ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() // TODO: move this somewhere else val requestPermissions = registerForActivityResult(requestPermissionActivityContract) { granted -> if (granted.containsAll(PERMISSIONS)) { Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show() } else { Toast.makeText(this, "Missing permissions!", Toast.LENGTH_SHORT).show() } } suspend fun checkPermissionsAndRun(healthConnectClient: HealthConnectClient): Boolean { val granted = healthConnectClient.permissionController.getGrantedPermissions() if (!granted.containsAll(PERMISSIONS)) requestPermissions.launch(PERMISSIONS) else return true return false } setContent { val healthConnectClient = getHealthConnectClient(LocalContext.current) var permissionsGranted = false LaunchedEffect(healthConnectClient) { if (healthConnectClient != null) { permissionsGranted = checkPermissionsAndRun(healthConnectClient) } } AppTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Column( modifier = Modifier.padding(innerPadding) ) { Text(text = "Are permissions granted?: $permissionsGranted") Graph( healthConnectClient = healthConnectClient!!, // TODO: check non-null defaultPeriod = GraphPeriod.MONTH ) } } } } } } /*** * Gets [HealthConnectClient] or returns `null` if unsupported */ private fun getHealthConnectClient(context: Context): HealthConnectClient? { val providerPackageName = "com.google.android.apps.healthdata" val availabilityStatus = HealthConnectClient.getSdkStatus(context, providerPackageName) if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE) { return null } if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) { // Optionally redirect to package installer to find a provider, for example: val uriString = "market://details?id=$providerPackageName&url=healthconnect%3A%2F%2Fonboarding" context.startActivity( Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(uriString) putExtra("overlay", true) putExtra("callerId", context.packageName) } ) return null } return HealthConnectClient.getOrCreate(context) } enum class GraphPeriod { DAY, WEEK, MONTH } @Composable fun Graph(healthConnectClient: HealthConnectClient, defaultPeriod: GraphPeriod = GraphPeriod.WEEK) { var period by remember { mutableStateOf(defaultPeriod) } Column( horizontalAlignment = Alignment.CenterHorizontally ) { PeriodSelector(defaultPeriod) { selection -> period = selection } GenericGraph(healthConnectClient, period) } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun PeriodSelector(startPeriod: GraphPeriod, onClick: (GraphPeriod) -> Unit) { val options = GraphPeriod.entries SingleChoiceSegmentedButtonRow { options.forEachIndexed { index, period -> SegmentedButton( selected = (period == startPeriod), onClick = { onClick(period) }, // TODO: simplify shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size), ) { Text( text = when (period) { GraphPeriod.DAY -> "Day" GraphPeriod.WEEK -> "Week" GraphPeriod.MONTH -> "Month" } ) } } } } @Composable fun GenericGraph(healthConnectClient: HealthConnectClient, period: GraphPeriod) { val data = remember { mutableListOf() } val endTime = Instant.now().truncatedTo(ChronoUnit.DAYS) val startTime = when (period) { GraphPeriod.DAY -> endTime GraphPeriod.WEEK -> endTime.minus(6, ChronoUnit.DAYS) GraphPeriod.MONTH -> endTime.minus(30, ChronoUnit.DAYS) // or 31, 28, 29? i fucking hate calendars } val range = when (period) { GraphPeriod.DAY -> 0L..23L GraphPeriod.WEEK -> 0L..6L GraphPeriod.MONTH -> 0L..29L } val unit = when (period) { GraphPeriod.DAY -> ChronoUnit.HOURS GraphPeriod.WEEK -> ChronoUnit.DAYS GraphPeriod.MONTH -> ChronoUnit.DAYS } for (i in range) { val dayStartTime = startTime.plus(i, unit) // TODO: rename val dayEndTime = dayStartTime.plus(1, unit) // TODO: rename var dataPoint: Long? runBlocking { dataPoint = readDataPoint(healthConnectClient, StepsRecord.COUNT_TOTAL, dayStartTime, dayEndTime) } if (dataPoint != null) { data.add(dataPoint!!.toFloat()) } else { data.add(0f) } } BarChartView( dataSet = ChartDataSet( items = data, title = "Steps" ) ) } suspend fun readDataPoint( healthConnectClient: HealthConnectClient, aggregateMetric: AggregateMetric, startTime: Instant, endTime: Instant ): T? { try { val response = healthConnectClient.aggregate( AggregateRequest( metrics = setOf(aggregateMetric), timeRangeFilter = TimeRangeFilter.between(startTime, endTime) ) ) return response[aggregateMetric] } catch (_: Exception) { return null } } ```
dautovicharis commented 3 weeks ago

Thank you for the additional information! I'll fix this as soon as I find the time. Currently, I'm working on a new feature that will most likely be available next week.

pacjo commented 3 weeks ago

Awesome! Take you're time.