feat: add GrowthLineChart composable

This commit is contained in:
Cutiful 2025-07-10 18:18:58 +07:00
parent b2d0620219
commit 75b5397932

View File

@ -0,0 +1,165 @@
package com.syaroful.agrilinkvocpro.growth_recipe_feature.core.component
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.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
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.presentation.theme.MainGreen
import kotlin.math.round
import kotlin.math.roundToInt
@Composable
fun GrowthLineChart(
dates: List<Int> = emptyList(),
values: List<Double> = emptyList(),
horizontalValue: Double? = null,
modifier: Modifier = Modifier,
graphColor: Color = MainGreen
) {
Box(
modifier = Modifier
.background(
color = Color.Transparent,
shape = RoundedCornerShape(8.dp)
)
.padding(top = 32.dp)
.padding(8.dp)
) {
val spacing = 80f
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 spacePerDate = (size.width - spacing) / values.size
dates.forEachIndexed { i, date ->
drawContext.canvas.nativeCanvas.drawText(
date.toString(),
spacing + i * spacePerDate,
size.height - 5,
textPaint
)
}
val step = (upperValue - lowerValue) / 5f
(0..4).forEach { i ->
drawContext.canvas.nativeCanvas.drawText(
round(lowerValue + step * 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 * spacePerDate
val y1 = height - spacing - (leftRatio * height).toFloat()
val x2 = spacing + (i + 1) * spacePerDate
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
)
)
val labelPadding = 12.dp.toPx()
drawContext.canvas.nativeCanvas.drawText(
"ppm",
spacing / 2f,
labelPadding - 100,
textPaint
)
drawContext.canvas.nativeCanvas.drawText(
"date",
size.width - size.width,
size.height - 4,
textPaint
)
horizontalValue?.let { value ->
val ratio = (value - lowerValue) / (upperValue - lowerValue)
val y = size.height - spacing - (ratio * size.height).toFloat()
drawLine(
color = Color.Red,
start = Offset(spacing, y),
end = Offset(size.width, y),
strokeWidth = 2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f))
)
}
}
}
}