feat: add line chart composable

This commit is contained in:
Cutiful 2025-06-12 11:38:23 +07:00
parent bd4c5ea0f3
commit 27bc8f4100

View File

@ -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<Int> = emptyList(),
values: List<Double> = 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
)
)
}
}
}