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 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 14:21:16 +09:00
parent 3c8de65dab
commit 99c58382b4
@@ -355,6 +355,12 @@ private fun MetricBox(title: String, value: Double, unit: String, color: Color,
@Composable
private fun LineChartCard(label: String, data: List<Double>, 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,22 +386,70 @@ private fun LineChartCard(label: String, data: List<Double>, 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 yMin = minVal - range * 0.05
val yMax = maxVal + range * 0.05
val yRange = yMax - yMin
// 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 - minVal) / range * size.height).toFloat()
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)
}
}
}
// Unit label
Text(unit, fontSize = 8.sp, color = MlSecondaryText, modifier = Modifier.padding(start = yLabelWidth))
}
}
}
}