From 1be3979c845f02d7f6f310f213ca975c021b82c6 Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Sat, 21 Jun 2025 01:14:06 +1000 Subject: [PATCH] Fix/2100 graph labels (#2182) Co-authored-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../mesh/ui/metrics/DeviceMetrics.kt | 82 +++++++++++++++++-- .../mesh/ui/metrics/EnvironmentMetrics.kt | 33 ++++++-- .../mesh/ui/metrics/PowerMetrics.kt | 30 +++++-- .../mesh/ui/metrics/SignalMetrics.kt | 32 ++++++-- 4 files changed, 142 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt index 8aee0fcc4..aa068b7fa 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -58,12 +59,14 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.R +import com.geeksville.mesh.TelemetryProtos import com.geeksville.mesh.TelemetryProtos.Telemetry import com.geeksville.mesh.model.MetricsViewModel import com.geeksville.mesh.model.TimeFrame import com.geeksville.mesh.ui.common.components.BatteryInfo import com.geeksville.mesh.ui.common.components.OptionLabel import com.geeksville.mesh.ui.common.components.SlidingSelector +import com.geeksville.mesh.ui.common.theme.AppTheme import com.geeksville.mesh.ui.common.theme.Orange import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT import com.geeksville.mesh.ui.metrics.CommonCharts.MAX_PERCENT_VALUE @@ -71,6 +74,7 @@ import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC import com.geeksville.mesh.util.GraphUtil import com.geeksville.mesh.util.GraphUtil.createPath import com.geeksville.mesh.util.GraphUtil.plotPoint +import androidx.compose.ui.tooling.preview.PreviewLightDark private enum class Device(val color: Color) { BATTERY(Color.Green), @@ -139,6 +143,7 @@ private fun DeviceMetricsChart( selectedTime: TimeFrame, promptInfoDialog: () -> Unit ) { + val graphColor = MaterialTheme.colorScheme.onSurface ChartHeader(amount = telemetries.size) if (telemetries.isEmpty()) return @@ -151,21 +156,31 @@ private fun DeviceMetricsChart( } val timeDiff = newest.time - oldest.time - TimeLabels( - oldest = oldest.time, - newest = newest.time - ) - - Spacer(modifier = Modifier.height(16.dp)) - - val graphColor = MaterialTheme.colorScheme.onSurface - val scrollState = rememberScrollState() val screenWidth = LocalWindowInfo.current.containerSize.width val dp by remember(key1 = selectedTime) { mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong())) } + // Calculate visible time range based on scroll position and chart width + val visibleTimeRange = run { + val totalWidthPx = with(LocalDensity.current) { dp.toPx() } + val scrollPx = scrollState.value.toFloat() + val visibleWidthPx = with(LocalDensity.current) { screenWidth.toDp().toPx() } + val leftRatio = (scrollPx / totalWidthPx).coerceIn(0f, 1f) + val rightRatio = ((scrollPx + visibleWidthPx) / totalWidthPx).coerceIn(0f, 1f) + val visibleOldest = oldest.time + (timeDiff * leftRatio).toInt() + val visibleNewest = oldest.time + (timeDiff * rightRatio).toInt() + visibleOldest to visibleNewest + } + + TimeLabels( + oldest = visibleTimeRange.first, + newest = visibleTimeRange.second + ) + + Spacer(modifier = Modifier.height(16.dp)) + Row { Box( contentAlignment = Alignment.TopStart, @@ -265,6 +280,34 @@ private fun DeviceMetricsChart( Spacer(modifier = Modifier.height(16.dp)) } +@Suppress("detekt:MagicNumber") // fake data +@PreviewLightDark +@Composable +private fun DeviceMetricsChartPreview() { + val now = (System.currentTimeMillis() / 1000).toInt() + val telemetries = List(20) { i -> + Telemetry.newBuilder() + .setTime(now - (19 - i) * 60 * 60) // 1-hour intervals, oldest first + .setDeviceMetrics( + TelemetryProtos.DeviceMetrics.newBuilder() + .setBatteryLevel(80 - i) + .setVoltage(3.7f - i * 0.02f) + .setChannelUtilization(10f + i * 2) + .setAirUtilTx(5f + i) + .setUptimeSeconds(3600 + i * 300) + ) + .build() + } + AppTheme { + DeviceMetricsChart( + modifier = Modifier.height(400.dp), + telemetries = telemetries, + selectedTime = TimeFrame.TWENTY_FOUR_HOURS, + promptInfoDialog = {} + ) + } +} + @Composable private fun DeviceMetricsCard(telemetry: Telemetry) { val deviceMetrics = telemetry.deviceMetrics @@ -321,3 +364,24 @@ private fun DeviceMetricsCard(telemetry: Telemetry) { } } } + +@Suppress("detekt:MagicNumber") // fake data +@PreviewLightDark +@Composable +private fun DeviceMetricsCardPreview() { + val now = (System.currentTimeMillis() / 1000).toInt() + val telemetry = Telemetry.newBuilder() + .setTime(now) + .setDeviceMetrics( + TelemetryProtos.DeviceMetrics.newBuilder() + .setBatteryLevel(75) + .setVoltage(3.65f) + .setChannelUtilization(22.5f) + .setAirUtilTx(12.0f) + .setUptimeSeconds(7200) + ) + .build() + AppTheme { + DeviceMetricsCard(telemetry = telemetry) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt index 6585dacf6..e3f3de589 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Path import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -165,7 +166,6 @@ fun EnvironmentMetricsScreen( } } -/* TODO need to take the time to understand this. */ @SuppressLint("ConfigurationScreenWidthHeight") @Suppress("LongMethod") @Composable @@ -182,9 +182,30 @@ private fun EnvironmentMetricsChart( } val (oldest, newest) = graphData.times + val timeDiff = newest - oldest + + val scrollState = rememberScrollState() + val screenWidth = LocalConfiguration.current.screenWidthDp + val dp by remember(key1 = selectedTime) { + mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong())) + } + + // Calculate visible time range based on scroll position and chart width + val visibleTimeRange = run { + val density = LocalDensity.current + val totalWidthPx = with(density) { dp.toPx() } + val scrollPx = scrollState.value.toFloat() + val visibleWidthPx = with(density) { screenWidth.dp.toPx() } + val leftRatio = (scrollPx / totalWidthPx).coerceIn(0f, 1f) + val rightRatio = ((scrollPx + visibleWidthPx) / totalWidthPx).coerceIn(0f, 1f) + val visibleOldest = oldest + (timeDiff * leftRatio).toInt() + val visibleNewest = oldest + (timeDiff * rightRatio).toInt() + visibleOldest to visibleNewest + } + TimeLabels( - oldest = oldest, - newest = newest + oldest = visibleTimeRange.first, + newest = visibleTimeRange.second ) Spacer(modifier = Modifier.height(16.dp)) @@ -196,12 +217,6 @@ private fun EnvironmentMetricsChart( var min = rightMin var diff = rightMax - rightMin - val scrollState = rememberScrollState() - val screenWidth = LocalConfiguration.current.screenWidthDp - val timeDiff = newest - oldest - val dp by remember(key1 = selectedTime) { - mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong())) - } val shouldPlot = graphData.shouldPlot Row { diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt index 1b3ac360d..e6e3df677 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -157,9 +158,28 @@ private fun PowerMetricsChart( } val timeDiff = newest.time - oldest.time + val scrollState = rememberScrollState() + val screenWidth = LocalWindowInfo.current.containerSize.width + val dp by remember(key1 = selectedTime) { + mutableStateOf(selectedTime.dp(screenWidth, time = (newest.time - oldest.time).toLong())) + } + + // Calculate visible time range based on scroll position and chart width + val visibleTimeRange = run { + val density = LocalDensity.current + val totalWidthPx = with(density) { dp.toPx() } + val scrollPx = scrollState.value.toFloat() + val visibleWidthPx = with(density) { screenWidth.toDp().toPx() } + val leftRatio = (scrollPx / totalWidthPx).coerceIn(0f, 1f) + val rightRatio = ((scrollPx + visibleWidthPx) / totalWidthPx).coerceIn(0f, 1f) + val visibleOldest = oldest.time + (timeDiff * leftRatio).toInt() + val visibleNewest = oldest.time + (timeDiff * rightRatio).toInt() + visibleOldest to visibleNewest + } + TimeLabels( - oldest = oldest.time, - newest = newest.time + oldest = visibleTimeRange.first, + newest = visibleTimeRange.second ) Spacer(modifier = Modifier.height(16.dp)) @@ -168,12 +188,6 @@ private fun PowerMetricsChart( val currentDiff = Power.CURRENT.difference() val voltageDiff = Power.VOLTAGE.difference() - val scrollState = rememberScrollState() - val screenWidth = LocalWindowInfo.current.containerSize.width - val dp by remember(key1 = selectedTime) { - mutableStateOf(selectedTime.dp(screenWidth, time = (newest.time - oldest.time).toLong())) - } - Row { YAxisLabels( modifier = modifier.weight(weight = .1f), diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt index 8b9d88b82..d2ce4c35c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -148,9 +149,29 @@ private fun SignalMetricsChart( } val timeDiff = newest.rxTime - oldest.rxTime + val scrollState = rememberScrollState() + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp + val dp by remember(key1 = selectedTime) { + mutableStateOf(selectedTime.dp(screenWidth, time = (newest.rxTime - oldest.rxTime).toLong())) + } + + // Calculate visible time range based on scroll position and chart width + val visibleTimeRange = run { + val density = LocalDensity.current + val totalWidthPx = with(density) { dp.toPx() } + val scrollPx = scrollState.value.toFloat() + val visibleWidthPx = with(density) { screenWidth.dp.toPx() } + val leftRatio = (scrollPx / totalWidthPx).coerceIn(0f, 1f) + val rightRatio = ((scrollPx + visibleWidthPx) / totalWidthPx).coerceIn(0f, 1f) + val visibleOldest = oldest.rxTime + (timeDiff * leftRatio).toInt() + val visibleNewest = oldest.rxTime + (timeDiff * rightRatio).toInt() + visibleOldest to visibleNewest + } + TimeLabels( - oldest = oldest.rxTime, - newest = newest.rxTime + oldest = visibleTimeRange.first, + newest = visibleTimeRange.second ) Spacer(modifier = Modifier.height(16.dp)) @@ -159,13 +180,6 @@ private fun SignalMetricsChart( val snrDiff = Metric.SNR.difference() val rssiDiff = Metric.RSSI.difference() - val scrollState = rememberScrollState() - val configuration = LocalConfiguration.current - val screenWidth = configuration.screenWidthDp - val dp by remember(key1 = selectedTime) { - mutableStateOf(selectedTime.dp(screenWidth, time = (newest.rxTime - oldest.rxTime).toLong())) - } - Row { YAxisLabels( modifier = modifier.weight(weight = .1f),