From 9cfe72ec3cf414256e1bcb513ea18fce8b2055c4 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:11:53 -0500 Subject: [PATCH] fix(metrics): air-quality chart legend follows plotted data, not selection (#6145) Co-authored-by: Claude Opus 4.8 --- .../feature/node/metrics/AirQualityMetrics.kt | 11 ++++++++++- .../node/metrics/AirQualityMetricsTest.kt | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt index 547bd5340..e5933726e 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt @@ -97,6 +97,14 @@ internal enum class AirQuality(val labelRes: StringResource, val unit: String, v } } +/** + * The subset of [candidates] with at least one reading in [telemetries]. The chart only draws series that have data, so + * the legend must use this rather than the raw selection — otherwise a default-selected series (PM2.5) shows a legend + * entry on nodes that never report it (issue #5873). Internal so it can be unit-tested. + */ +internal fun metricsWithData(candidates: List, telemetries: List): List = + candidates.filter { metric -> telemetries.any { metric.getValue(it) != null } } + private val LEGEND_DATA = AirQuality.entries.map { metric -> LegendData(nameRes = metric.labelRes, color = metric.color, isLine = true) } @@ -193,10 +201,11 @@ private fun AirQualityChart( modifier: Modifier = Modifier, ) { val activeMetrics = AirQuality.entries.filter { it in selectedMetrics } + val drawnMetrics = metricsWithData(activeMetrics, telemetries) val metricLabels = activeMetrics.associateWith { stringResource(it.labelRes) } MetricChartScaffold( isEmpty = telemetries.isEmpty() || activeMetrics.isEmpty(), - legendData = LEGEND_DATA.filter { ld -> activeMetrics.any { it.labelRes == ld.nameRes } }, + legendData = LEGEND_DATA.filter { ld -> drawnMetrics.any { it.labelRes == ld.nameRes } }, modifier = modifier, ) { modelProducer, chartModifier -> val marker = diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetricsTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetricsTest.kt index 2ef873306..fe32cd12f 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetricsTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetricsTest.kt @@ -69,4 +69,21 @@ class AirQualityMetricsTest { assertNull(it.getValue(t), "${it.name} should be null without air_quality_metrics") } } + + @Test + fun `metricsWithData drops selected series that have no reading so the legend is not ever-present`() { + // Issue 5873, CO2-only node: PM2.5 is default-selected but never reported, so it must not survive into the + // legend. + val co2Only = listOf(telemetry(AirQualityMetrics(co2 = 450))) + assertEquals(listOf(AirQuality.CO2), metricsWithData(listOf(AirQuality.PM2_5, AirQuality.CO2), co2Only)) + } + + @Test + fun `metricsWithData keeps a series once it has any reading in the frame`() { + val mixed = listOf(telemetry(AirQualityMetrics(co2 = 450)), telemetry(AirQualityMetrics(pm25_standard = 8))) + assertEquals( + listOf(AirQuality.PM2_5, AirQuality.CO2), + metricsWithData(listOf(AirQuality.PM2_5, AirQuality.CO2), mixed), + ) + } }