mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-27 10:11:48 -04:00
refactor(metrics): Prevent chart crashes with empty data (#4578)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
@@ -226,7 +226,7 @@ fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
private fun DeviceMetricsChart(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -258,59 +258,82 @@ private fun DeviceMetricsChart(
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(telemetries) {
|
||||
val batteryData = remember(telemetries) { telemetries.filter { it.device_metrics?.battery_level != null } }
|
||||
val chUtilData = remember(telemetries) { telemetries.filter { it.device_metrics?.channel_utilization != null } }
|
||||
val airUtilData = remember(telemetries) { telemetries.filter { it.device_metrics?.air_util_tx != null } }
|
||||
val voltageData = remember(telemetries) { telemetries.filter { it.device_metrics?.voltage != null } }
|
||||
|
||||
val batteryStyle =
|
||||
if (batteryData.isNotEmpty()) {
|
||||
ChartStyling.createBoldLine(batteryColor, ChartStyling.MEDIUM_POINT_SIZE_DP)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val chUtilStyle =
|
||||
if (chUtilData.isNotEmpty()) {
|
||||
ChartStyling.createPointOnlyLine(chUtilColor, ChartStyling.LARGE_POINT_SIZE_DP)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val airUtilStyle =
|
||||
if (airUtilData.isNotEmpty()) {
|
||||
ChartStyling.createPointOnlyLine(airUtilColor, ChartStyling.LARGE_POINT_SIZE_DP)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val leftLayerSeriesStyles =
|
||||
remember(batteryStyle, chUtilStyle, airUtilStyle) { listOfNotNull(batteryStyle, chUtilStyle, airUtilStyle) }
|
||||
|
||||
LaunchedEffect(batteryData, chUtilData, airUtilData, voltageData, leftLayerSeriesStyles) {
|
||||
modelProducer.runTransaction {
|
||||
/* Series for Left Axis (0-100%) */
|
||||
lineSeries {
|
||||
series(
|
||||
x = telemetries.map { it.time ?: 0 },
|
||||
y = telemetries.map { it.device_metrics?.battery_level ?: 0 },
|
||||
)
|
||||
val chUtilData = telemetries.filter { it.device_metrics?.channel_utilization != null }
|
||||
series(
|
||||
x = chUtilData.map { it.time ?: 0 },
|
||||
y = chUtilData.map { it.device_metrics?.channel_utilization ?: 0f },
|
||||
)
|
||||
val airUtilData = telemetries.filter { it.device_metrics?.air_util_tx != null }
|
||||
series(
|
||||
x = airUtilData.map { it.time ?: 0 },
|
||||
y = airUtilData.map { it.device_metrics?.air_util_tx ?: 0f },
|
||||
)
|
||||
if (leftLayerSeriesStyles.isNotEmpty()) {
|
||||
lineSeries {
|
||||
if (batteryData.isNotEmpty()) {
|
||||
series(
|
||||
x = batteryData.map { it.time ?: 0 },
|
||||
y = batteryData.map { (it.device_metrics?.battery_level ?: 0).toFloat() },
|
||||
)
|
||||
}
|
||||
if (chUtilData.isNotEmpty()) {
|
||||
series(
|
||||
x = chUtilData.map { it.time ?: 0 },
|
||||
y = chUtilData.map { it.device_metrics?.channel_utilization ?: 0f },
|
||||
)
|
||||
}
|
||||
if (airUtilData.isNotEmpty()) {
|
||||
series(
|
||||
x = airUtilData.map { it.time ?: 0 },
|
||||
y = airUtilData.map { it.device_metrics?.air_util_tx ?: 0f },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Series for Right Axis (Voltage) */
|
||||
lineSeries {
|
||||
val voltageData = telemetries.filter { it.device_metrics?.voltage != null }
|
||||
series(
|
||||
x = voltageData.map { it.time ?: 0 },
|
||||
y = voltageData.map { it.device_metrics?.voltage ?: 0f },
|
||||
)
|
||||
if (voltageData.isNotEmpty()) {
|
||||
lineSeries {
|
||||
series(
|
||||
x = voltageData.map { it.time ?: 0 },
|
||||
y = voltageData.map { it.device_metrics?.voltage ?: 0f },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GenericMetricChart(
|
||||
modelProducer = modelProducer,
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp).padding(bottom = 0.dp),
|
||||
layers =
|
||||
listOf(
|
||||
val leftLayer =
|
||||
if (leftLayerSeriesStyles.isNotEmpty()) {
|
||||
rememberLineCartesianLayer(
|
||||
lineProvider =
|
||||
LineCartesianLayer.LineProvider.series(
|
||||
ChartStyling.createBoldLine(
|
||||
lineColor = batteryColor,
|
||||
pointSize = ChartStyling.MEDIUM_POINT_SIZE_DP,
|
||||
),
|
||||
ChartStyling.createPointOnlyLine(
|
||||
pointColor = chUtilColor,
|
||||
pointSize = ChartStyling.LARGE_POINT_SIZE_DP,
|
||||
),
|
||||
ChartStyling.createPointOnlyLine(
|
||||
pointColor = airUtilColor,
|
||||
pointSize = ChartStyling.LARGE_POINT_SIZE_DP,
|
||||
),
|
||||
),
|
||||
lineProvider = LineCartesianLayer.LineProvider.series(leftLayerSeriesStyles),
|
||||
verticalAxisPosition = Axis.Position.Vertical.Start,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val rightLayer =
|
||||
if (voltageData.isNotEmpty()) {
|
||||
rememberLineCartesianLayer(
|
||||
lineProvider =
|
||||
LineCartesianLayer.LineProvider.series(
|
||||
@@ -320,30 +343,49 @@ private fun DeviceMetricsChart(
|
||||
),
|
||||
),
|
||||
verticalAxisPosition = Axis.Position.Vertical.End,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val layers = remember(leftLayer, rightLayer) { listOfNotNull(leftLayer, rightLayer) }
|
||||
|
||||
if (layers.isNotEmpty()) {
|
||||
GenericMetricChart(
|
||||
modelProducer = modelProducer,
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp).padding(bottom = 0.dp),
|
||||
layers = layers,
|
||||
startAxis =
|
||||
if (leftLayer != null) {
|
||||
VerticalAxis.rememberStart(
|
||||
label = ChartStyling.rememberAxisLabel(color = batteryColor),
|
||||
valueFormatter = { _, value, _ -> "%.0f%%".format(value) },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
endAxis =
|
||||
if (rightLayer != null) {
|
||||
VerticalAxis.rememberEnd(
|
||||
label = ChartStyling.rememberAxisLabel(color = voltageColor),
|
||||
valueFormatter = { _, value, _ -> "%.1f V".format(value) },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
bottomAxis =
|
||||
HorizontalAxis.rememberBottom(
|
||||
label = ChartStyling.rememberAxisLabel(),
|
||||
valueFormatter = CommonCharts.dynamicTimeFormatter,
|
||||
itemPlacer = ChartStyling.rememberItemPlacer(spacing = 20),
|
||||
labelRotationDegrees = 45f,
|
||||
),
|
||||
),
|
||||
startAxis =
|
||||
VerticalAxis.rememberStart(
|
||||
label = ChartStyling.rememberAxisLabel(color = batteryColor),
|
||||
valueFormatter = { _, value, _ -> "%.0f%%".format(value) },
|
||||
),
|
||||
endAxis =
|
||||
VerticalAxis.rememberEnd(
|
||||
label = ChartStyling.rememberAxisLabel(color = voltageColor),
|
||||
valueFormatter = { _, value, _ -> "%.1f V".format(value) },
|
||||
),
|
||||
bottomAxis =
|
||||
HorizontalAxis.rememberBottom(
|
||||
label = ChartStyling.rememberAxisLabel(),
|
||||
valueFormatter = CommonCharts.dynamicTimeFormatter,
|
||||
itemPlacer = ChartStyling.rememberItemPlacer(spacing = 20),
|
||||
labelRotationDegrees = 45f,
|
||||
),
|
||||
marker = marker,
|
||||
selectedX = selectedX,
|
||||
onPointSelected = onPointSelected,
|
||||
vicoScrollState = vicoScrollState,
|
||||
)
|
||||
marker = marker,
|
||||
selectedX = selectedX,
|
||||
onPointSelected = onPointSelected,
|
||||
vicoScrollState = vicoScrollState,
|
||||
)
|
||||
}
|
||||
|
||||
Legend(legendData = legendData, modifier = Modifier.padding(top = 0.dp))
|
||||
}
|
||||
|
||||
@@ -129,16 +129,41 @@ fun EnvironmentMetricsChart(
|
||||
}
|
||||
val colorToLabel = allLegendData.associate { it.color to stringResource(it.nameRes) }
|
||||
|
||||
LaunchedEffect(telemetries, graphData) {
|
||||
val pressureData =
|
||||
remember(telemetries) {
|
||||
telemetries.filter {
|
||||
val v = Environment.BAROMETRIC_PRESSURE.getValue(it)
|
||||
it.time != 0 && v != null && !v.isNaN()
|
||||
}
|
||||
}
|
||||
|
||||
val otherMetrics =
|
||||
remember(telemetries, shouldPlot) {
|
||||
Environment.entries.filter { metric ->
|
||||
metric != Environment.BAROMETRIC_PRESSURE &&
|
||||
shouldPlot[metric.ordinal] &&
|
||||
telemetries.any {
|
||||
val v = metric.getValue(it)
|
||||
it.time != 0 && v != null && !v.isNaN()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val otherMetricsData =
|
||||
remember(telemetries, otherMetrics) {
|
||||
otherMetrics.associateWith { metric ->
|
||||
telemetries.filter {
|
||||
val v = metric.getValue(it)
|
||||
it.time != 0 && v != null && !v.isNaN()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(pressureData, otherMetricsData) {
|
||||
modelProducer.runTransaction {
|
||||
/* Pressure on its own layer/axis */
|
||||
if (shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal]) {
|
||||
if (shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal] && pressureData.isNotEmpty()) {
|
||||
lineSeries {
|
||||
val pressureData =
|
||||
telemetries.filter {
|
||||
val v = Environment.BAROMETRIC_PRESSURE.getValue(it)
|
||||
it.time != 0 && v != null && !v.isNaN()
|
||||
}
|
||||
series(
|
||||
x = pressureData.map { it.time },
|
||||
y = pressureData.map { Environment.BAROMETRIC_PRESSURE.getValue(it)!! },
|
||||
@@ -146,14 +171,10 @@ fun EnvironmentMetricsChart(
|
||||
}
|
||||
}
|
||||
/* Everything else on the default axis */
|
||||
Environment.entries.forEach { metric ->
|
||||
if (metric != Environment.BAROMETRIC_PRESSURE && shouldPlot[metric.ordinal]) {
|
||||
otherMetrics.forEach { metric ->
|
||||
val metricData = otherMetricsData[metric] ?: emptyList()
|
||||
if (metricData.isNotEmpty()) {
|
||||
lineSeries {
|
||||
val metricData =
|
||||
telemetries.filter {
|
||||
val v = metric.getValue(it)
|
||||
it.time != 0 && v != null && !v.isNaN()
|
||||
}
|
||||
series(x = metricData.map { it.time }, y = metricData.map { metric.getValue(it)!! })
|
||||
}
|
||||
}
|
||||
@@ -171,7 +192,7 @@ fun EnvironmentMetricsChart(
|
||||
)
|
||||
|
||||
val layers = mutableListOf<LineCartesianLayer>()
|
||||
if (shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal]) {
|
||||
if (shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal] && pressureData.isNotEmpty()) {
|
||||
layers.add(
|
||||
rememberLineCartesianLayer(
|
||||
lineProvider =
|
||||
@@ -185,31 +206,27 @@ fun EnvironmentMetricsChart(
|
||||
),
|
||||
)
|
||||
}
|
||||
Environment.entries.forEach { metric ->
|
||||
if (metric != Environment.BAROMETRIC_PRESSURE && shouldPlot[metric.ordinal]) {
|
||||
layers.add(
|
||||
rememberLineCartesianLayer(
|
||||
lineProvider =
|
||||
LineCartesianLayer.LineProvider.series(
|
||||
ChartStyling.createGradientLine(metric.color, ChartStyling.MEDIUM_POINT_SIZE_DP),
|
||||
),
|
||||
verticalAxisPosition = Axis.Position.Vertical.End,
|
||||
otherMetrics.forEach { metric ->
|
||||
layers.add(
|
||||
rememberLineCartesianLayer(
|
||||
lineProvider =
|
||||
LineCartesianLayer.LineProvider.series(
|
||||
ChartStyling.createGradientLine(metric.color, ChartStyling.MEDIUM_POINT_SIZE_DP),
|
||||
),
|
||||
)
|
||||
}
|
||||
verticalAxisPosition = Axis.Position.Vertical.End,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (layers.isNotEmpty()) {
|
||||
val otherMetricsPlotted =
|
||||
Environment.entries.filter { it != Environment.BAROMETRIC_PRESSURE && shouldPlot[it.ordinal] }
|
||||
val endAxisColor = if (otherMetricsPlotted.size == 1) otherMetricsPlotted.first().color else onSurfaceColor
|
||||
val endAxisColor = if (otherMetrics.size == 1) otherMetrics.first().color else onSurfaceColor
|
||||
|
||||
GenericMetricChart(
|
||||
modelProducer = modelProducer,
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp).padding(bottom = 0.dp),
|
||||
layers = layers,
|
||||
startAxis =
|
||||
if (shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal]) {
|
||||
if (shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal] && pressureData.isNotEmpty()) {
|
||||
VerticalAxis.rememberStart(
|
||||
label = ChartStyling.rememberAxisLabel(color = Environment.BAROMETRIC_PRESSURE.color),
|
||||
valueFormatter = { _, value, _ -> "%.0f hPa".format(value) },
|
||||
|
||||
@@ -197,15 +197,17 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNav
|
||||
|
||||
// Prepare data for graph
|
||||
val graphData =
|
||||
paxMetrics
|
||||
.map {
|
||||
val t = (it.first.received_date / CommonCharts.MS_PER_SEC).toInt()
|
||||
Triple(t, it.second.ble ?: 0, it.second.wifi ?: 0)
|
||||
}
|
||||
.sortedBy { it.first }
|
||||
val totalSeries = graphData.map { it.first to (it.second + it.third) }
|
||||
val bleSeries = graphData.map { it.first to it.second }
|
||||
val wifiSeries = graphData.map { it.first to it.third }
|
||||
remember(paxMetrics) {
|
||||
paxMetrics
|
||||
.map {
|
||||
val t = (it.first.received_date / CommonCharts.MS_PER_SEC).toInt()
|
||||
Triple(t, it.second.ble ?: 0, it.second.wifi ?: 0)
|
||||
}
|
||||
.sortedBy { it.first }
|
||||
}
|
||||
val totalSeries = remember(graphData) { graphData.map { it.first to (it.second + it.third) } }
|
||||
val bleSeries = remember(graphData) { graphData.map { it.first to it.second } }
|
||||
val wifiSeries = remember(graphData) { graphData.map { it.first to it.third } }
|
||||
|
||||
BaseMetricScreen(
|
||||
onNavigateUp = onNavigateUp,
|
||||
|
||||
@@ -212,67 +212,100 @@ private fun PowerMetricsChart(
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(telemetries, selectedChannel) {
|
||||
val currentData =
|
||||
remember(telemetries, selectedChannel) {
|
||||
telemetries.filter { !retrieveCurrent(selectedChannel, it).isNaN() }
|
||||
}
|
||||
val voltageData =
|
||||
remember(telemetries, selectedChannel) {
|
||||
telemetries.filter { !retrieveVoltage(selectedChannel, it).isNaN() }
|
||||
}
|
||||
|
||||
LaunchedEffect(currentData, voltageData) {
|
||||
modelProducer.runTransaction {
|
||||
lineSeries {
|
||||
val currentData = telemetries.filter { !retrieveCurrent(selectedChannel, it).isNaN() }
|
||||
series(
|
||||
x = currentData.map { it.time ?: 0 },
|
||||
y = currentData.map { retrieveCurrent(selectedChannel, it) },
|
||||
)
|
||||
if (currentData.isNotEmpty()) {
|
||||
lineSeries {
|
||||
series(
|
||||
x = currentData.map { it.time ?: 0 },
|
||||
y = currentData.map { retrieveCurrent(selectedChannel, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
lineSeries {
|
||||
val voltageData = telemetries.filter { !retrieveVoltage(selectedChannel, it).isNaN() }
|
||||
series(
|
||||
x = voltageData.map { it.time ?: 0 },
|
||||
y = voltageData.map { retrieveVoltage(selectedChannel, it) },
|
||||
)
|
||||
if (voltageData.isNotEmpty()) {
|
||||
lineSeries {
|
||||
series(
|
||||
x = voltageData.map { it.time ?: 0 },
|
||||
y = voltageData.map { retrieveVoltage(selectedChannel, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GenericMetricChart(
|
||||
modelProducer = modelProducer,
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp).padding(bottom = 0.dp),
|
||||
layers =
|
||||
listOf(
|
||||
val currentLayer =
|
||||
if (currentData.isNotEmpty()) {
|
||||
rememberLineCartesianLayer(
|
||||
lineProvider =
|
||||
LineCartesianLayer.LineProvider.series(
|
||||
ChartStyling.createBoldLine(currentColor, ChartStyling.MEDIUM_POINT_SIZE_DP),
|
||||
),
|
||||
verticalAxisPosition = Axis.Position.Vertical.Start,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val voltageLayer =
|
||||
if (voltageData.isNotEmpty()) {
|
||||
rememberLineCartesianLayer(
|
||||
lineProvider =
|
||||
LineCartesianLayer.LineProvider.series(
|
||||
ChartStyling.createGradientLine(voltageColor, ChartStyling.MEDIUM_POINT_SIZE_DP),
|
||||
),
|
||||
verticalAxisPosition = Axis.Position.Vertical.End,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val layers = remember(currentLayer, voltageLayer) { listOfNotNull(currentLayer, voltageLayer) }
|
||||
|
||||
if (layers.isNotEmpty()) {
|
||||
GenericMetricChart(
|
||||
modelProducer = modelProducer,
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp).padding(bottom = 0.dp),
|
||||
layers = layers,
|
||||
startAxis =
|
||||
if (currentData.isNotEmpty()) {
|
||||
VerticalAxis.rememberStart(
|
||||
label = ChartStyling.rememberAxisLabel(color = currentColor),
|
||||
valueFormatter = { _, value, _ -> "%.0f mA".format(value) },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
endAxis =
|
||||
if (voltageData.isNotEmpty()) {
|
||||
VerticalAxis.rememberEnd(
|
||||
label = ChartStyling.rememberAxisLabel(color = voltageColor),
|
||||
valueFormatter = { _, value, _ -> "%.1f V".format(value) },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
bottomAxis =
|
||||
HorizontalAxis.rememberBottom(
|
||||
label = ChartStyling.rememberAxisLabel(),
|
||||
valueFormatter = CommonCharts.dynamicTimeFormatter,
|
||||
itemPlacer = ChartStyling.rememberItemPlacer(spacing = 50),
|
||||
labelRotationDegrees = 45f,
|
||||
),
|
||||
),
|
||||
startAxis =
|
||||
VerticalAxis.rememberStart(
|
||||
label = ChartStyling.rememberAxisLabel(color = currentColor),
|
||||
valueFormatter = { _, value, _ -> "%.0f mA".format(value) },
|
||||
),
|
||||
endAxis =
|
||||
VerticalAxis.rememberEnd(
|
||||
label = ChartStyling.rememberAxisLabel(color = voltageColor),
|
||||
valueFormatter = { _, value, _ -> "%.1f V".format(value) },
|
||||
),
|
||||
bottomAxis =
|
||||
HorizontalAxis.rememberBottom(
|
||||
label = ChartStyling.rememberAxisLabel(),
|
||||
valueFormatter = CommonCharts.dynamicTimeFormatter,
|
||||
itemPlacer = ChartStyling.rememberItemPlacer(spacing = 50),
|
||||
labelRotationDegrees = 45f,
|
||||
),
|
||||
marker = marker,
|
||||
selectedX = selectedX,
|
||||
onPointSelected = onPointSelected,
|
||||
vicoScrollState = vicoScrollState,
|
||||
)
|
||||
marker = marker,
|
||||
selectedX = selectedX,
|
||||
onPointSelected = onPointSelected,
|
||||
vicoScrollState = vicoScrollState,
|
||||
)
|
||||
}
|
||||
|
||||
Legend(legendData = LEGEND_DATA, modifier = Modifier.padding(top = 0.dp))
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ fun SignalMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
private fun SignalMetricsChart(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -162,24 +162,24 @@ private fun SignalMetricsChart(
|
||||
if (meshPackets.isEmpty()) return@Column
|
||||
|
||||
val modelProducer = remember { CartesianChartModelProducer() }
|
||||
val rssiColor = SignalMetric.RSSI.color
|
||||
val snrColor = SignalMetric.SNR.color
|
||||
|
||||
LaunchedEffect(meshPackets) {
|
||||
val rssiData = remember(meshPackets) { meshPackets.filter { (it.rx_rssi ?: 0) != 0 } }
|
||||
val snrData = remember(meshPackets) { meshPackets.filter { !((it.rx_snr ?: Float.NaN).isNaN()) } }
|
||||
|
||||
LaunchedEffect(rssiData, snrData) {
|
||||
modelProducer.runTransaction {
|
||||
/* Use separate lineSeries calls to associate them with different vertical axes */
|
||||
lineSeries {
|
||||
val rssiData = meshPackets.filter { (it.rx_rssi ?: 0) != 0 }
|
||||
series(x = rssiData.map { it.rx_time ?: 0 }, y = rssiData.map { it.rx_rssi ?: 0 })
|
||||
if (rssiData.isNotEmpty()) {
|
||||
/* Use separate lineSeries calls to associate them with different vertical axes */
|
||||
lineSeries { series(x = rssiData.map { it.rx_time ?: 0 }, y = rssiData.map { it.rx_rssi ?: 0 }) }
|
||||
}
|
||||
lineSeries {
|
||||
val snrData = meshPackets.filter { !((it.rx_snr ?: Float.NaN).isNaN()) }
|
||||
series(x = snrData.map { it.rx_time ?: 0 }, y = snrData.map { it.rx_snr ?: 0f })
|
||||
if (snrData.isNotEmpty()) {
|
||||
lineSeries { series(x = snrData.map { it.rx_time ?: 0 }, y = snrData.map { it.rx_snr ?: 0f }) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val rssiColor = SignalMetric.RSSI.color
|
||||
val snrColor = SignalMetric.SNR.color
|
||||
|
||||
val marker =
|
||||
ChartStyling.rememberMarker(
|
||||
valueFormatter =
|
||||
@@ -192,48 +192,70 @@ private fun SignalMetricsChart(
|
||||
},
|
||||
)
|
||||
|
||||
GenericMetricChart(
|
||||
modelProducer = modelProducer,
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp).padding(bottom = 0.dp),
|
||||
layers =
|
||||
listOf(
|
||||
val rssiLayer =
|
||||
if (rssiData.isNotEmpty()) {
|
||||
rememberLineCartesianLayer(
|
||||
lineProvider =
|
||||
LineCartesianLayer.LineProvider.series(
|
||||
ChartStyling.createPointOnlyLine(rssiColor, ChartStyling.LARGE_POINT_SIZE_DP),
|
||||
),
|
||||
verticalAxisPosition = Axis.Position.Vertical.Start,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val snrLayer =
|
||||
if (snrData.isNotEmpty()) {
|
||||
rememberLineCartesianLayer(
|
||||
lineProvider =
|
||||
LineCartesianLayer.LineProvider.series(
|
||||
ChartStyling.createPointOnlyLine(snrColor, ChartStyling.LARGE_POINT_SIZE_DP),
|
||||
),
|
||||
verticalAxisPosition = Axis.Position.Vertical.End,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val layers = remember(rssiLayer, snrLayer) { listOfNotNull(rssiLayer, snrLayer) }
|
||||
|
||||
if (layers.isNotEmpty()) {
|
||||
GenericMetricChart(
|
||||
modelProducer = modelProducer,
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp).padding(bottom = 0.dp),
|
||||
layers = layers,
|
||||
startAxis =
|
||||
if (rssiData.isNotEmpty()) {
|
||||
VerticalAxis.rememberStart(
|
||||
label = ChartStyling.rememberAxisLabel(color = rssiColor),
|
||||
valueFormatter = { _, value, _ -> "%.0f dBm".format(value) },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
endAxis =
|
||||
if (snrData.isNotEmpty()) {
|
||||
VerticalAxis.rememberEnd(
|
||||
label = ChartStyling.rememberAxisLabel(color = snrColor),
|
||||
valueFormatter = { _, value, _ -> "%.1f dB".format(value) },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
bottomAxis =
|
||||
HorizontalAxis.rememberBottom(
|
||||
label = ChartStyling.rememberAxisLabel(),
|
||||
valueFormatter = CommonCharts.dynamicTimeFormatter,
|
||||
itemPlacer = ChartStyling.rememberItemPlacer(spacing = 50),
|
||||
labelRotationDegrees = 45f,
|
||||
),
|
||||
),
|
||||
startAxis =
|
||||
VerticalAxis.rememberStart(
|
||||
label = ChartStyling.rememberAxisLabel(color = rssiColor),
|
||||
valueFormatter = { _, value, _ -> "%.0f dBm".format(value) },
|
||||
),
|
||||
endAxis =
|
||||
VerticalAxis.rememberEnd(
|
||||
label = ChartStyling.rememberAxisLabel(color = snrColor),
|
||||
valueFormatter = { _, value, _ -> "%.1f dB".format(value) },
|
||||
),
|
||||
bottomAxis =
|
||||
HorizontalAxis.rememberBottom(
|
||||
label = ChartStyling.rememberAxisLabel(),
|
||||
valueFormatter = CommonCharts.dynamicTimeFormatter,
|
||||
itemPlacer = ChartStyling.rememberItemPlacer(spacing = 50),
|
||||
labelRotationDegrees = 45f,
|
||||
),
|
||||
marker = marker,
|
||||
selectedX = selectedX,
|
||||
onPointSelected = onPointSelected,
|
||||
vicoScrollState = vicoScrollState,
|
||||
)
|
||||
marker = marker,
|
||||
selectedX = selectedX,
|
||||
onPointSelected = onPointSelected,
|
||||
vicoScrollState = vicoScrollState,
|
||||
)
|
||||
}
|
||||
|
||||
Legend(legendData = LEGEND_DATA, modifier = Modifier.padding(top = 0.dp))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user