From 99c58382b4830793549e19389b8c02d48add227f Mon Sep 17 00:00:00 2001 From: jjangddu Date: Mon, 16 Mar 2026 14:21:16 +0900 Subject: [PATCH] feat: add X/Y axis labels and grid lines to NIRS charts Y-axis shows data range with 5 tick labels, X-axis shows elapsed time in seconds, horizontal grid lines for readability. Co-Authored-By: Claude Opus 4.6 --- .../views/monitoring/VivamyoMonitoringView.kt | 78 ++++++++++++++++--- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VivamyoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VivamyoMonitoringView.kt index bfd8d08..75ffb71 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VivamyoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/VivamyoMonitoringView.kt @@ -355,6 +355,12 @@ private fun MetricBox(title: String, value: Double, unit: String, color: Color, @Composable private fun LineChartCard(label: String, data: List, color: Color, unit: String) { + val axisColor = MlSecondaryText.copy(alpha = 0.4f) + val gridColor = MlSecondaryText.copy(alpha = 0.1f) + val sampleRate = 4f // Hz + val yLabelWidth = 36.dp + val xLabelHeight = 18.dp + Card( shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = MlCardBackground), @@ -380,21 +386,69 @@ private fun LineChartCard(label: String, data: List, color: Color, unit: contentAlignment = Alignment.Center ) { Text("No data", fontSize = 11.sp, color = MlSecondaryText) } } else { - Canvas(modifier = Modifier.fillMaxWidth().height(140.dp)) { - if (data.size < 2) return@Canvas - val minVal = data.min() - val maxVal = data.max() - val range = if (maxVal - minVal > 0.001) maxVal - minVal else 1.0 - val stepX = size.width / (data.size - 1).toFloat() + val minVal = data.min() + val maxVal = data.max() + val range = if (maxVal - minVal > 0.001) maxVal - minVal else 1.0 + val yMin = minVal - range * 0.05 + val yMax = maxVal + range * 0.05 + val yRange = yMax - yMin - val path = Path() - data.forEachIndexed { index, value -> - val x = index * stepX - val y = size.height - ((value - minVal) / range * size.height).toFloat() - if (index == 0) path.moveTo(x, y) else path.lineTo(x, y) + // Y-axis tick values (5 ticks) + val yTicks = (0..4).map { yMin + yRange * it / 4.0 } + + Row(modifier = Modifier.fillMaxWidth().height(140.dp + xLabelHeight)) { + // Y-axis labels + Column( + modifier = Modifier.width(yLabelWidth).height(140.dp), + verticalArrangement = Arrangement.SpaceBetween + ) { + yTicks.reversed().forEach { tick -> + Text( + String.format("%.1f", tick), + fontSize = 8.sp, + color = MlSecondaryText, + maxLines = 1 + ) + } + } + + Column { + // Chart area + Canvas(modifier = Modifier.fillMaxWidth().height(140.dp)) { + // Grid lines + for (i in 0..4) { + val y = size.height * i / 4f + drawLine(gridColor, Offset(0f, y), Offset(size.width, y), strokeWidth = 1f) + } + + // Data line + if (data.size >= 2) { + val stepX = size.width / (data.size - 1).toFloat() + val path = Path() + data.forEachIndexed { index, value -> + val x = index * stepX + val y = size.height - ((value - yMin) / yRange * size.height).toFloat() + if (index == 0) path.moveTo(x, y) else path.lineTo(x, y) + } + drawPath(path, color = color, style = Stroke(width = 3f)) + } + } + + // X-axis labels + val totalSeconds = data.size / sampleRate + Row( + modifier = Modifier.fillMaxWidth().height(xLabelHeight), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(String.format("%.0fs", 0f), fontSize = 8.sp, color = MlSecondaryText) + Text(String.format("%.0fs", totalSeconds / 2), fontSize = 8.sp, color = MlSecondaryText) + Text(String.format("%.0fs", totalSeconds), fontSize = 8.sp, color = MlSecondaryText) + } } - drawPath(path, color = color, style = Stroke(width = 3f)) } + + // Unit label + Text(unit, fontSize = 8.sp, color = MlSecondaryText, modifier = Modifier.padding(start = yLabelWidth)) } } }