diff --git a/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/core/component/GrowthLineChart.kt b/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/core/component/GrowthLineChart.kt new file mode 100644 index 0000000..78d271a --- /dev/null +++ b/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/core/component/GrowthLineChart.kt @@ -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 = emptyList(), + values: List = 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)) + ) + } + } + } +} \ No newline at end of file