From 27bc8f410095056011e6a871fb885c82e5294223 Mon Sep 17 00:00:00 2001 From: Cutiful <113351087+Syaroful@users.noreply.github.com> Date: Thu, 12 Jun 2025 11:38:23 +0700 Subject: [PATCH] feat: add line chart composable --- .../ui/screen/detail/LineChart.kt | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/ui/screen/detail/LineChart.kt diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/ui/screen/detail/LineChart.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/ui/screen/detail/LineChart.kt new file mode 100644 index 0000000..2ca9d8c --- /dev/null +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/ui/screen/detail/LineChart.kt @@ -0,0 +1,135 @@ +package com.syaroful.agrilinkvocpro.ui.screen.detail + +import android.graphics.Paint +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.asAndroidPath +import androidx.compose.ui.graphics.asComposePath +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.syaroful.agrilinkvocpro.ui.theme.MainGreen +import kotlin.math.round +import kotlin.math.roundToInt + +@Composable +fun LineChart( + hours: List = emptyList(), + values: List = emptyList(), + modifier: Modifier = Modifier, + graphColor: Color = MainGreen +) { + Box( + modifier = Modifier + .background(color = MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(8.dp)) + .padding(top = 20.dp) + .padding(8.dp) + ) { + val spacing = 100f + val transparentGraphColor = remember { + graphColor.copy(alpha = 0.5f) + } + val upperValue = remember(values) { + (values.maxOrNull()?.plus(1))?.roundToInt() ?: 0 + } + val lowerValue = remember(values) { + values.minOrNull()?.toInt() ?: 0 + } + val density = LocalDensity.current + val isDarkMode = isSystemInDarkTheme() + val textPaint = remember(density) { + Paint().apply { + color = + if (isDarkMode) android.graphics.Color.WHITE else android.graphics.Color.BLACK + textAlign = Paint.Align.CENTER + textSize = density.run { 12.sp.toPx() } + } + } + + Canvas(modifier = modifier) { + val spacePerHour = (size.width - spacing) / values.size + (0 until hours.size - 1 step 2).forEach { i -> + val value = values[i] + val hour = hours[i] + drawContext.canvas.nativeCanvas.apply { + drawText( + hour.toString(), + spacing + i * spacePerHour, + size.height - 5, + textPaint + ) + } + } + val priceStep = (upperValue - lowerValue) / 5f + (0..4).forEach { i -> + drawContext.canvas.nativeCanvas.apply { + drawText( + round(lowerValue + priceStep * i).toString(), + 30f, + size.height - spacing - i * size.height / 5f, + textPaint + ) + } + } + var lastX = 0f + val strokePath = Path().apply { + val height = size.height + for (i in values.indices) { + val value = values[i] + val nextValue = values.getOrNull(i + 1) ?: values.last() + val leftRatio = (value - lowerValue) / (upperValue - lowerValue) + val rightRatio = (nextValue - lowerValue) / (upperValue - lowerValue) + + val x1 = spacing + i * spacePerHour + val y1 = height - spacing - (leftRatio * height).toFloat() + val x2 = spacing + (i + 1) * spacePerHour + val y2 = height - spacing - (rightRatio * height).toFloat() + if (i == 0) { + moveTo(x1, y1) + } + lastX = (x1 + x2) / 2f + quadraticTo(x1, y1, lastX, (y1 + y2) / 2f) + } + } + val fillPath = android.graphics.Path(strokePath.asAndroidPath()) + .asComposePath() + .apply { + lineTo(lastX, size.height - spacing) + lineTo(spacing, size.height - spacing) + close() + } + drawPath( + path = fillPath, + brush = Brush.verticalGradient( + colors = listOf( + transparentGraphColor, + Color.Transparent + ), + endY = size.height - spacing + ) + ) + drawPath( + path = strokePath, + color = graphColor, + style = Stroke( + width = 3.dp.toPx(), + cap = StrokeCap.Round + ) + ) + } + } +} \ No newline at end of file