mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-12 16:55:02 -04:00
Fix/2100 graph labels (#2188)
Co-authored-by: Dane Evans <dane@goneepic.com>
This commit is contained in:
@@ -57,14 +57,16 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT
|
||||
import com.geeksville.mesh.ui.metrics.CommonCharts.MAX_PERCENT_VALUE
|
||||
import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
|
||||
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_MINUTE_FORMAT
|
||||
import java.text.DateFormat
|
||||
|
||||
object CommonCharts {
|
||||
val DATE_TIME_FORMAT: DateFormat =
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
||||
val TIME_MINUTE_FORMAT: DateFormat = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
val DATE_TIME_MINUTE_FORMAT: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
|
||||
const val MS_PER_SEC = 1000L
|
||||
const val MAX_PERCENT_VALUE = 100f
|
||||
}
|
||||
@@ -190,7 +192,7 @@ fun TimeAxisOverlay(
|
||||
Canvas(modifier = modifier) {
|
||||
|
||||
val height = size.height
|
||||
val width = size.width - 28.dp.toPx()
|
||||
val width = size.width
|
||||
|
||||
/* Cut out the time remaining in order to place the lines on the dot. */
|
||||
val timeRemaining = oldest % timeInterval
|
||||
@@ -247,18 +249,19 @@ fun TimeAxisOverlay(
|
||||
@Composable
|
||||
fun TimeLabels(
|
||||
oldest: Int,
|
||||
newest: Int
|
||||
newest: Int,
|
||||
) {
|
||||
|
||||
Row {
|
||||
Text(
|
||||
text = DATE_TIME_FORMAT.format(oldest * MS_PER_SEC),
|
||||
text = DATE_TIME_MINUTE_FORMAT.format(oldest * MS_PER_SEC),
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = DATE_TIME_FORMAT.format(newest * MS_PER_SEC),
|
||||
text = DATE_TIME_MINUTE_FORMAT.format(newest * MS_PER_SEC),
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||
fontSize = 12.sp
|
||||
|
||||
@@ -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),
|
||||
@@ -78,6 +82,10 @@ private enum class Device(val color: Color) {
|
||||
AIR_UTIL(Color.Cyan)
|
||||
}
|
||||
|
||||
private const val CHART_WEIGHT = 1f
|
||||
private const val Y_AXIS_WEIGHT = 0.1f
|
||||
private const val CHART_WIDTH_RATIO = CHART_WEIGHT / (CHART_WEIGHT + Y_AXIS_WEIGHT)
|
||||
|
||||
private val LEGEND_DATA = listOf(
|
||||
LegendData(nameRes = R.string.battery, color = Device.BATTERY.color, isLine = true),
|
||||
LegendData(nameRes = R.string.channel_utilization, color = Device.CH_UTIL.color),
|
||||
@@ -139,6 +147,7 @@ private fun DeviceMetricsChart(
|
||||
selectedTime: TimeFrame,
|
||||
promptInfoDialog: () -> Unit
|
||||
) {
|
||||
val graphColor = MaterialTheme.colorScheme.onSurface
|
||||
|
||||
ChartHeader(amount = telemetries.size)
|
||||
if (telemetries.isEmpty()) return
|
||||
@@ -151,21 +160,33 @@ 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()
|
||||
// Calculate visible width based on actual weight distribution
|
||||
val visibleWidthPx = screenWidth * CHART_WIDTH_RATIO
|
||||
val leftRatio = (scrollPx / totalWidthPx).coerceIn(0f, 1f)
|
||||
val rightRatio = ((scrollPx + visibleWidthPx) / totalWidthPx).coerceIn(0f, 1f)
|
||||
// With reverseScrolling = true, scrolling right shows older data (left side of chart)
|
||||
val visibleOldest = oldest.time + (timeDiff * (1f - rightRatio)).toInt()
|
||||
val visibleNewest = oldest.time + (timeDiff * (1f - leftRatio)).toInt()
|
||||
visibleOldest to visibleNewest
|
||||
}
|
||||
|
||||
TimeLabels(
|
||||
oldest = visibleTimeRange.first,
|
||||
newest = visibleTimeRange.second,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row {
|
||||
Box(
|
||||
contentAlignment = Alignment.TopStart,
|
||||
@@ -252,7 +273,7 @@ private fun DeviceMetricsChart(
|
||||
}
|
||||
}
|
||||
YAxisLabels(
|
||||
modifier = modifier.weight(weight = .1f),
|
||||
modifier = modifier.weight(weight = Y_AXIS_WEIGHT),
|
||||
graphColor,
|
||||
minValue = 0f,
|
||||
maxValue = 100f
|
||||
@@ -265,6 +286,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 +370,86 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt:MagicNumber") // fake data
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun DeviceMetricsScreenPreview() {
|
||||
val now = (System.currentTimeMillis() / 1000).toInt()
|
||||
val telemetries = List(24) { i ->
|
||||
Telemetry.newBuilder()
|
||||
.setTime(now - (23 - i) * 60 * 60) // 1-hour intervals, oldest first
|
||||
.setDeviceMetrics(
|
||||
TelemetryProtos.DeviceMetrics.newBuilder()
|
||||
.setBatteryLevel(85 - i * 2) // Battery decreases over time
|
||||
.setVoltage(3.8f - i * 0.01f) // Voltage decreases slightly
|
||||
.setChannelUtilization(15f + i * 1.5f) // Channel utilization increases
|
||||
.setAirUtilTx(8f + i * 0.8f) // Air utilization increases
|
||||
.setUptimeSeconds(3600 + i * 3600) // Uptime increases by 1 hour each
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
AppTheme {
|
||||
Surface {
|
||||
Column {
|
||||
var displayInfoDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (displayInfoDialog) {
|
||||
LegendInfoDialog(
|
||||
pairedRes = listOf(
|
||||
Pair(R.string.channel_utilization, R.string.ch_util_definition),
|
||||
Pair(R.string.air_utilization, R.string.air_util_definition)
|
||||
),
|
||||
onDismiss = { displayInfoDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
DeviceMetricsChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(fraction = 0.33f),
|
||||
telemetries.reversed(),
|
||||
TimeFrame.TWENTY_FOUR_HOURS,
|
||||
promptInfoDialog = { displayInfoDialog = true }
|
||||
)
|
||||
|
||||
SlidingSelector(
|
||||
TimeFrame.entries.toList(),
|
||||
TimeFrame.TWENTY_FOUR_HOURS,
|
||||
onOptionSelected = { /* Preview only */ }
|
||||
) {
|
||||
OptionLabel(stringResource(it.strRes))
|
||||
}
|
||||
|
||||
/* Device Metric Cards */
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(telemetries) { telemetry -> DeviceMetricsCard(telemetry) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,10 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -72,6 +74,20 @@ import com.geeksville.mesh.util.GraphUtil.createPath
|
||||
import com.geeksville.mesh.util.GraphUtil.drawPathWithGradient
|
||||
import com.geeksville.mesh.util.UnitConversions.celsiusToFahrenheit
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private enum class Environment(val color: Color) {
|
||||
TEMPERATURE(Color.Red),
|
||||
RELATIVE_HUMIDITY(Color.Blue),
|
||||
BAROMETRIC_PRESSURE(Color.Green),
|
||||
GAS_RESISTANCE(Color.Yellow),
|
||||
IAQ(Color.Magenta)
|
||||
}
|
||||
|
||||
private const val CHART_WEIGHT = 1f
|
||||
private const val Y_AXIS_WEIGHT = 0.1f
|
||||
// EnvironmentMetrics can have 1 or 2 Y-axis labels depending on whether barometric pressure is plotted
|
||||
// We'll calculate this dynamically in the chart function
|
||||
|
||||
private val LEGEND_DATA_1 = listOf(
|
||||
LegendData(
|
||||
nameRes = R.string.temperature,
|
||||
@@ -165,7 +181,6 @@ fun EnvironmentMetricsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO need to take the time to understand this. */
|
||||
@SuppressLint("ConfigurationScreenWidthHeight")
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
@@ -182,9 +197,35 @@ private fun EnvironmentMetricsChart(
|
||||
}
|
||||
|
||||
val (oldest, newest) = graphData.times
|
||||
val timeDiff = newest - oldest
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val screenWidth = LocalWindowInfo.current.containerSize.width
|
||||
val dp by remember(key1 = selectedTime) {
|
||||
mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong()))
|
||||
}
|
||||
|
||||
val shouldPlot = graphData.shouldPlot
|
||||
|
||||
// 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()
|
||||
// Calculate chart width ratio dynamically based on whether barometric pressure is plotted
|
||||
val yAxisCount = if (shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal]) 2 else 1
|
||||
val chartWidthRatio = CHART_WEIGHT / (CHART_WEIGHT + (Y_AXIS_WEIGHT * yAxisCount))
|
||||
val visibleWidthPx = screenWidth * chartWidthRatio
|
||||
val leftRatio = (scrollPx / totalWidthPx).coerceIn(0f, 1f)
|
||||
val rightRatio = ((scrollPx + visibleWidthPx) / totalWidthPx).coerceIn(0f, 1f)
|
||||
// With reverseScrolling = true, scrolling right shows older data (left side of chart)
|
||||
val visibleOldest = oldest + (timeDiff * (1f - rightRatio)).toInt()
|
||||
val visibleNewest = oldest + (timeDiff * (1f - leftRatio)).toInt()
|
||||
visibleOldest to visibleNewest
|
||||
}
|
||||
|
||||
TimeLabels(
|
||||
oldest = oldest,
|
||||
newest = newest
|
||||
oldest = visibleTimeRange.first,
|
||||
newest = visibleTimeRange.second
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -196,18 +237,10 @@ 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 {
|
||||
if (shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal]) {
|
||||
YAxisLabels(
|
||||
modifier = modifier.weight(weight = .1f),
|
||||
modifier = modifier.weight(weight = Y_AXIS_WEIGHT),
|
||||
Environment.BAROMETRIC_PRESSURE.color,
|
||||
minValue = pressureMin,
|
||||
maxValue = pressureMax
|
||||
@@ -277,7 +310,7 @@ private fun EnvironmentMetricsChart(
|
||||
}
|
||||
}
|
||||
YAxisLabels(
|
||||
modifier = modifier.weight(weight = .1f),
|
||||
modifier = modifier.weight(weight = Y_AXIS_WEIGHT),
|
||||
graphColor,
|
||||
minValue = rightMin,
|
||||
maxValue = rightMax
|
||||
|
||||
@@ -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
|
||||
@@ -87,6 +88,10 @@ private enum class PowerChannel(@StringRes val strRes: Int) {
|
||||
THREE(R.string.channel_3)
|
||||
}
|
||||
|
||||
private const val CHART_WEIGHT = 1f
|
||||
private const val Y_AXIS_WEIGHT = 0.1f
|
||||
private const val CHART_WIDTH_RATIO = CHART_WEIGHT / (CHART_WEIGHT + Y_AXIS_WEIGHT + Y_AXIS_WEIGHT)
|
||||
|
||||
private val LEGEND_DATA = listOf(
|
||||
LegendData(nameRes = R.string.current, color = Power.CURRENT.color, isLine = true),
|
||||
LegendData(nameRes = R.string.voltage, color = Power.VOLTAGE.color, isLine = true),
|
||||
@@ -157,9 +162,29 @@ 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 totalWidthPx = with(LocalDensity.current) { dp.toPx() }
|
||||
val scrollPx = scrollState.value.toFloat()
|
||||
// Calculate visible width based on actual weight distribution
|
||||
val visibleWidthPx = screenWidth * CHART_WIDTH_RATIO
|
||||
val leftRatio = (scrollPx / totalWidthPx).coerceIn(0f, 1f)
|
||||
val rightRatio = ((scrollPx + visibleWidthPx) / totalWidthPx).coerceIn(0f, 1f)
|
||||
// With reverseScrolling = true, scrolling right shows older data (left side of chart)
|
||||
val visibleOldest = oldest.time + (timeDiff * (1f - rightRatio)).toInt()
|
||||
val visibleNewest = oldest.time + (timeDiff * (1f - leftRatio)).toInt()
|
||||
visibleOldest to visibleNewest
|
||||
}
|
||||
|
||||
TimeLabels(
|
||||
oldest = oldest.time,
|
||||
newest = newest.time
|
||||
oldest = visibleTimeRange.first,
|
||||
newest = visibleTimeRange.second
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -168,15 +193,9 @@ 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),
|
||||
modifier = modifier.weight(weight = Y_AXIS_WEIGHT),
|
||||
Power.CURRENT.color,
|
||||
minValue = Power.CURRENT.min,
|
||||
maxValue = Power.CURRENT.max,
|
||||
@@ -263,7 +282,7 @@ private fun PowerMetricsChart(
|
||||
}
|
||||
}
|
||||
YAxisLabels(
|
||||
modifier = modifier.weight(weight = .1f),
|
||||
modifier = modifier.weight(weight = Y_AXIS_WEIGHT),
|
||||
Power.VOLTAGE.color,
|
||||
minValue = Power.VOLTAGE.min,
|
||||
maxValue = Power.VOLTAGE.max,
|
||||
|
||||
@@ -48,7 +48,8 @@ import androidx.compose.runtime.setValue
|
||||
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.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -76,6 +77,11 @@ private enum class Metric(val color: Color, val min: Float, val max: Float) {
|
||||
*/
|
||||
fun difference() = max - min
|
||||
}
|
||||
|
||||
private const val CHART_WEIGHT = 1f
|
||||
private const val Y_AXIS_WEIGHT = 0.1f
|
||||
private const val CHART_WIDTH_RATIO = CHART_WEIGHT / (CHART_WEIGHT + Y_AXIS_WEIGHT + Y_AXIS_WEIGHT)
|
||||
|
||||
private val LEGEND_DATA = listOf(
|
||||
LegendData(nameRes = R.string.rssi, color = Metric.RSSI.color),
|
||||
LegendData(nameRes = R.string.snr, color = Metric.SNR.color)
|
||||
@@ -148,9 +154,29 @@ private fun SignalMetricsChart(
|
||||
}
|
||||
val timeDiff = newest.rxTime - oldest.rxTime
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val screenWidth = LocalWindowInfo.current.containerSize.width
|
||||
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 totalWidthPx = with(LocalDensity.current) { dp.toPx() }
|
||||
val scrollPx = scrollState.value.toFloat()
|
||||
// Calculate visible width based on actual weight distribution
|
||||
val visibleWidthPx = screenWidth * CHART_WIDTH_RATIO
|
||||
val leftRatio = (scrollPx / totalWidthPx).coerceIn(0f, 1f)
|
||||
val rightRatio = ((scrollPx + visibleWidthPx) / totalWidthPx).coerceIn(0f, 1f)
|
||||
// With reverseScrolling = true, scrolling right shows older data (left side of chart)
|
||||
val visibleOldest = oldest.rxTime + (timeDiff * (1f - rightRatio)).toInt()
|
||||
val visibleNewest = oldest.rxTime + (timeDiff * (1f - leftRatio)).toInt()
|
||||
visibleOldest to visibleNewest
|
||||
}
|
||||
|
||||
TimeLabels(
|
||||
oldest = oldest.rxTime,
|
||||
newest = newest.rxTime
|
||||
oldest = visibleTimeRange.first,
|
||||
newest = visibleTimeRange.second
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -159,16 +185,9 @@ 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),
|
||||
modifier = modifier.weight(weight = Y_AXIS_WEIGHT),
|
||||
Metric.RSSI.color,
|
||||
minValue = Metric.RSSI.min,
|
||||
maxValue = Metric.RSSI.max,
|
||||
@@ -221,7 +240,7 @@ private fun SignalMetricsChart(
|
||||
}
|
||||
}
|
||||
YAxisLabels(
|
||||
modifier = modifier.weight(weight = .1f),
|
||||
modifier = modifier.weight(weight = Y_AXIS_WEIGHT),
|
||||
Metric.SNR.color,
|
||||
minValue = Metric.SNR.min,
|
||||
maxValue = Metric.SNR.max,
|
||||
|
||||
Reference in New Issue
Block a user