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:
+58
-4
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user