kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat: Scrollable x axis (#1445)
* Started horizontal scrolling for DeviceMetrics. Drawing lines based on the TimeFrame and setting the dp. * Wrote YAxisLabels(), it will replace the Y labels portion of the ChartOverlay(). The composable works for either side of the graph. * Wrote HorizontalLinesOverlay(), it will replace the horizontal lines portion of the ChartOverlay(). * Updated the data points to use their actual x values. * Based the width of the scrollable graph on time. * Added a date label to the TimeAxisOverlay.pull/1321/head
rodzic
3c581f81a8
commit
b3f4929cf4
|
@ -21,6 +21,8 @@ import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
@ -120,6 +122,48 @@ enum class TimeFrame(
|
||||||
} else {
|
} else {
|
||||||
System.currentTimeMillis() / 1000 - this.seconds
|
System.currentTimeMillis() / 1000 - this.seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time interval to draw the vertical lines representing
|
||||||
|
* time on the x-axis.
|
||||||
|
*
|
||||||
|
* @return seconds epoch seconds
|
||||||
|
*/
|
||||||
|
fun lineInterval(): Long {
|
||||||
|
return when (this.ordinal) {
|
||||||
|
TWENTY_FOUR_HOURS.ordinal,
|
||||||
|
FORTY_EIGHT_HOURS.ordinal ->
|
||||||
|
TimeUnit.HOURS.toSeconds(1)
|
||||||
|
ONE_WEEK.ordinal,
|
||||||
|
TWO_WEEKS.ordinal ->
|
||||||
|
TimeUnit.DAYS.toSeconds(1)
|
||||||
|
else ->
|
||||||
|
TimeUnit.DAYS.toSeconds(7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the needed [Dp] depending on the amount of time being plotted.
|
||||||
|
*
|
||||||
|
* @param time in seconds
|
||||||
|
*/
|
||||||
|
fun dp(screenWidth: Int, time: Long): Dp {
|
||||||
|
|
||||||
|
val timePerScreen = when (this.ordinal) {
|
||||||
|
TWENTY_FOUR_HOURS.ordinal,
|
||||||
|
FORTY_EIGHT_HOURS.ordinal ->
|
||||||
|
TimeUnit.HOURS.toSeconds(1)
|
||||||
|
ONE_WEEK.ordinal,
|
||||||
|
TWO_WEEKS.ordinal ->
|
||||||
|
TimeUnit.DAYS.toSeconds(1)
|
||||||
|
else ->
|
||||||
|
TimeUnit.DAYS.toSeconds(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
val multiplier = time / timePerScreen
|
||||||
|
val dp = (screenWidth * multiplier).toInt().dp
|
||||||
|
return dp.takeIf { it != 0.dp } ?: screenWidth.dp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MeshPacket.hasValidSignal(): Boolean =
|
private fun MeshPacket.hasValidSignal(): Boolean =
|
||||||
|
|
|
@ -59,21 +59,25 @@ import androidx.compose.ui.unit.sp
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.LINE_LIMIT
|
import com.geeksville.mesh.ui.components.CommonCharts.LINE_LIMIT
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.TEXT_PAINT_ALPHA
|
import com.geeksville.mesh.ui.components.CommonCharts.TEXT_PAINT_ALPHA
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.TIME_FORMAT
|
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.LEFT_LABEL_SPACING
|
import com.geeksville.mesh.ui.components.CommonCharts.LEFT_LABEL_SPACING
|
||||||
|
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
|
||||||
object CommonCharts {
|
object CommonCharts {
|
||||||
val TIME_FORMAT: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
val DATE_TIME_FORMAT: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
||||||
const val X_AXIS_SPACING = 8f
|
const val X_AXIS_SPACING = 8f
|
||||||
const val LEFT_LABEL_SPACING = 36
|
const val LEFT_LABEL_SPACING = 36
|
||||||
const val MS_PER_SEC = 1000.0f
|
const val MS_PER_SEC = 1000L
|
||||||
const val LINE_LIMIT = 4
|
const val LINE_LIMIT = 4
|
||||||
const val TEXT_PAINT_ALPHA = 192
|
const val TEXT_PAINT_ALPHA = 192
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val TIME_FORMAT: DateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM)
|
||||||
|
private val DATE_FORMAT: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
|
||||||
private const val LINE_ON = 10f
|
private const val LINE_ON = 10f
|
||||||
private const val LINE_OFF = 20f
|
private const val LINE_OFF = 20f
|
||||||
|
private const val DATE_Y = 32f
|
||||||
|
|
||||||
data class LegendData(val nameRes: Int, val color: Color, val isLine: Boolean = false)
|
data class LegendData(val nameRes: Int, val color: Color, val isLine: Boolean = false)
|
||||||
|
|
||||||
|
@ -100,6 +104,7 @@ fun ChartHeader(amount: Int) {
|
||||||
* @param lineColors A list of 5 `Color`s for the chart lines, 0 being the lowest line on the chart.
|
* @param lineColors A list of 5 `Color`s for the chart lines, 0 being the lowest line on the chart.
|
||||||
* @param leaveSpace When true the lines will leave space for Y labels on the left side of the graph.
|
* @param leaveSpace When true the lines will leave space for Y labels on the left side of the graph.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Will soon be replaced with YAxisLabels() and HorizontalLines()", level = DeprecationLevel.WARNING)
|
||||||
@Composable
|
@Composable
|
||||||
fun ChartOverlay(
|
fun ChartOverlay(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
|
@ -160,14 +165,162 @@ fun ChartOverlay(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws chart lines with respect to the Y-axis range; defined by (`maxValue` - `minValue`).
|
||||||
|
*
|
||||||
|
* @param lineColors A list of 5 `Color`s for the chart lines, 0 being the lowest line on the chart.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun HorizontalLinesOverlay(
|
||||||
|
modifier: Modifier,
|
||||||
|
lineColors: List<Color>,
|
||||||
|
minValue: Float,
|
||||||
|
maxValue: Float,
|
||||||
|
) {
|
||||||
|
val range = maxValue - minValue
|
||||||
|
val verticalSpacing = range / LINE_LIMIT
|
||||||
|
Canvas(modifier = modifier) {
|
||||||
|
|
||||||
|
val lineStart = 0f
|
||||||
|
val height = size.height
|
||||||
|
val width = size.width
|
||||||
|
|
||||||
|
/* Horizontal Lines */
|
||||||
|
var lineY = minValue
|
||||||
|
for (i in 0..LINE_LIMIT) {
|
||||||
|
val ratio = (lineY - minValue) / range
|
||||||
|
val y = height - (ratio * height)
|
||||||
|
drawLine(
|
||||||
|
start = Offset(lineStart, y),
|
||||||
|
end = Offset(width, y),
|
||||||
|
color = lineColors[i],
|
||||||
|
strokeWidth = 1.dp.toPx(),
|
||||||
|
cap = StrokeCap.Round,
|
||||||
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(LINE_ON, LINE_OFF), 0f)
|
||||||
|
)
|
||||||
|
lineY += verticalSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws labels on the Y-axis with respect to the range. Defined by (`maxValue` - `minValue`).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun YAxisLabels(
|
||||||
|
modifier: Modifier,
|
||||||
|
labelColor: Color,
|
||||||
|
minValue: Float,
|
||||||
|
maxValue: Float,
|
||||||
|
) {
|
||||||
|
val range = maxValue - minValue
|
||||||
|
val verticalSpacing = range / LINE_LIMIT
|
||||||
|
val density = LocalDensity.current
|
||||||
|
Canvas(modifier = modifier) {
|
||||||
|
|
||||||
|
val height = size.height
|
||||||
|
|
||||||
|
/* Y Labels */
|
||||||
|
val textPaint = Paint().apply {
|
||||||
|
color = labelColor.toArgb()
|
||||||
|
textAlign = Paint.Align.LEFT
|
||||||
|
textSize = density.run { 12.dp.toPx() }
|
||||||
|
typeface = setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD))
|
||||||
|
alpha = TEXT_PAINT_ALPHA
|
||||||
|
}
|
||||||
|
|
||||||
|
drawContext.canvas.nativeCanvas.apply {
|
||||||
|
var label = minValue
|
||||||
|
for (i in 0..LINE_LIMIT) {
|
||||||
|
val ratio = (label - minValue) / range
|
||||||
|
val y = height - (ratio * height)
|
||||||
|
drawText(
|
||||||
|
"${label.toInt()}",
|
||||||
|
0f,
|
||||||
|
y + 4.dp.toPx(),
|
||||||
|
textPaint
|
||||||
|
)
|
||||||
|
label += verticalSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the vertical lines to help the user relate the plotted data within a time frame.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TimeAxisOverlay(
|
||||||
|
modifier: Modifier,
|
||||||
|
oldest: Int,
|
||||||
|
newest: Int,
|
||||||
|
timeInterval: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
val range = newest - oldest
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val lineColor = MaterialTheme.colors.onSurface
|
||||||
|
Canvas(modifier = modifier) {
|
||||||
|
|
||||||
|
val height = size.height
|
||||||
|
val width = size.width - 28.dp.toPx()
|
||||||
|
|
||||||
|
/* Cut out the time remaining in order to place the lines on the dot. */
|
||||||
|
val timeRemaining = oldest % timeInterval
|
||||||
|
var current = oldest.toLong()
|
||||||
|
current -= timeRemaining
|
||||||
|
current += timeInterval
|
||||||
|
|
||||||
|
val textPaint = Paint().apply {
|
||||||
|
color = lineColor.toArgb()
|
||||||
|
textAlign = Paint.Align.LEFT
|
||||||
|
textSize = density.run { 12.dp.toPx() }
|
||||||
|
typeface = setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD))
|
||||||
|
alpha = TEXT_PAINT_ALPHA
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical Lines with labels */
|
||||||
|
drawContext.canvas.nativeCanvas.apply {
|
||||||
|
while (current <= newest) {
|
||||||
|
val ratio = (current - oldest).toFloat() / range
|
||||||
|
val x = (ratio * width)
|
||||||
|
drawLine(
|
||||||
|
start = Offset(x, 0f),
|
||||||
|
end = Offset(x, height),
|
||||||
|
color = lineColor,
|
||||||
|
strokeWidth = 1.dp.toPx(),
|
||||||
|
cap = StrokeCap.Round,
|
||||||
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(LINE_ON, LINE_OFF), 0f)
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Time */
|
||||||
|
drawText(
|
||||||
|
TIME_FORMAT.format(current * MS_PER_SEC),
|
||||||
|
x,
|
||||||
|
0f,
|
||||||
|
textPaint
|
||||||
|
)
|
||||||
|
/* Date */
|
||||||
|
drawText(
|
||||||
|
DATE_FORMAT.format(current * MS_PER_SEC),
|
||||||
|
x,
|
||||||
|
DATE_Y,
|
||||||
|
textPaint
|
||||||
|
)
|
||||||
|
current += timeInterval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the `oldest` and `newest` times for the respective telemetry data.
|
* Draws the `oldest` and `newest` times for the respective telemetry data.
|
||||||
* Expects time in milliseconds
|
* Expects time in seconds.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun TimeLabels(
|
fun TimeLabels(
|
||||||
oldest: Float,
|
oldest: Int,
|
||||||
newest: Float
|
newest: Int
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
@ -175,14 +328,14 @@ fun TimeLabels(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = TIME_FORMAT.format(oldest),
|
text = DATE_TIME_FORMAT.format(oldest * MS_PER_SEC),
|
||||||
modifier = Modifier.wrapContentWidth(),
|
modifier = Modifier.wrapContentWidth(),
|
||||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Text(
|
Text(
|
||||||
text = TIME_FORMAT.format(newest),
|
text = DATE_TIME_FORMAT.format(newest * MS_PER_SEC),
|
||||||
modifier = Modifier.wrapContentWidth(),
|
modifier = Modifier.wrapContentWidth(),
|
||||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||||
fontSize = 12.sp
|
fontSize = 12.sp
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package com.geeksville.mesh.ui.components
|
package com.geeksville.mesh.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
@ -28,8 +29,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.Card
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
|
@ -48,6 +51,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
@ -57,10 +61,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
||||||
import com.geeksville.mesh.model.MetricsViewModel
|
import com.geeksville.mesh.model.MetricsViewModel
|
||||||
|
import com.geeksville.mesh.model.TimeFrame
|
||||||
import com.geeksville.mesh.ui.BatteryInfo
|
import com.geeksville.mesh.ui.BatteryInfo
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.X_AXIS_SPACING
|
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.TIME_FORMAT
|
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||||
import com.geeksville.mesh.ui.theme.Orange
|
import com.geeksville.mesh.ui.theme.Orange
|
||||||
|
|
||||||
private val DEVICE_METRICS_COLORS = listOf(Color.Green, Color.Magenta, Color.Cyan)
|
private val DEVICE_METRICS_COLORS = listOf(Color.Green, Color.Magenta, Color.Cyan)
|
||||||
|
@ -102,6 +106,7 @@ fun DeviceMetricsScreen(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(fraction = 0.33f),
|
.fillMaxHeight(fraction = 0.33f),
|
||||||
data.reversed(),
|
data.reversed(),
|
||||||
|
selectedTimeFrame,
|
||||||
promptInfoDialog = { displayInfoDialog = true }
|
promptInfoDialog = { displayInfoDialog = true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -126,95 +131,133 @@ fun DeviceMetricsScreen(
|
||||||
private fun DeviceMetricsChart(
|
private fun DeviceMetricsChart(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
telemetries: List<Telemetry>,
|
telemetries: List<Telemetry>,
|
||||||
|
selectedTime: TimeFrame,
|
||||||
promptInfoDialog: () -> Unit
|
promptInfoDialog: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
ChartHeader(amount = telemetries.size)
|
ChartHeader(amount = telemetries.size)
|
||||||
if (telemetries.isEmpty()) return
|
if (telemetries.isEmpty()) return
|
||||||
|
|
||||||
|
val (oldest, newest) = remember(key1 = telemetries) {
|
||||||
|
Pair(
|
||||||
|
telemetries.minBy { it.time },
|
||||||
|
telemetries.maxBy { it.time }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val timeDiff = newest.time - oldest.time
|
||||||
|
|
||||||
TimeLabels(
|
TimeLabels(
|
||||||
oldest = telemetries.first().time * MS_PER_SEC,
|
oldest = oldest.time,
|
||||||
newest = telemetries.last().time * MS_PER_SEC
|
newest = newest.time
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
val graphColor = MaterialTheme.colors.onSurface
|
val graphColor = MaterialTheme.colors.onSurface
|
||||||
val spacing = X_AXIS_SPACING
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
Box(contentAlignment = Alignment.TopStart) {
|
val configuration = LocalConfiguration.current
|
||||||
|
val screenWidth = configuration.screenWidthDp
|
||||||
|
val dp by remember(key1 = selectedTime) {
|
||||||
|
mutableStateOf(selectedTime.dp(screenWidth, time = (newest.time - oldest.time).toLong()))
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
Row {
|
||||||
* The order of the colors are with respect to the ChUtil.
|
Box(
|
||||||
* 25 - 49 Orange
|
contentAlignment = Alignment.TopStart,
|
||||||
* 50 - 100 Red
|
modifier = Modifier
|
||||||
*/
|
.horizontalScroll(scrollState)
|
||||||
ChartOverlay(
|
.weight(1f)
|
||||||
modifier,
|
) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The order of the colors are with respect to the ChUtil.
|
||||||
|
* 25 - 49 Orange
|
||||||
|
* 50 - 100 Red
|
||||||
|
*/
|
||||||
|
HorizontalLinesOverlay(
|
||||||
|
modifier.width(dp),
|
||||||
|
lineColors = listOf(graphColor, Orange, Color.Red, graphColor, graphColor),
|
||||||
|
minValue = 0f,
|
||||||
|
maxValue = 100f
|
||||||
|
)
|
||||||
|
|
||||||
|
TimeAxisOverlay(
|
||||||
|
modifier.width(dp),
|
||||||
|
oldest = oldest.time,
|
||||||
|
newest = newest.time,
|
||||||
|
selectedTime.lineInterval()
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Plot Battery Line, ChUtil, and AirUtilTx */
|
||||||
|
Canvas(modifier = modifier.width(dp)) {
|
||||||
|
|
||||||
|
val height = size.height
|
||||||
|
val width = size.width
|
||||||
|
val dataPointRadius = 2.dp.toPx()
|
||||||
|
val strokePath = Path().apply {
|
||||||
|
for (i in telemetries.indices) {
|
||||||
|
val telemetry = telemetries[i]
|
||||||
|
|
||||||
|
/* x-value for all three */
|
||||||
|
val x1Ratio = (telemetry.time - oldest.time).toFloat() / timeDiff
|
||||||
|
val x1 = x1Ratio * width
|
||||||
|
|
||||||
|
/* Channel Utilization */
|
||||||
|
val chUtilRatio =
|
||||||
|
telemetry.deviceMetrics.channelUtilization / MAX_PERCENT_VALUE
|
||||||
|
val yChUtil = height - (chUtilRatio * height)
|
||||||
|
drawCircle(
|
||||||
|
color = DEVICE_METRICS_COLORS[Device.CH_UTIL.ordinal],
|
||||||
|
radius = dataPointRadius,
|
||||||
|
center = Offset(x1, yChUtil)
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Air Utilization Transmit */
|
||||||
|
val airUtilRatio = telemetry.deviceMetrics.airUtilTx / MAX_PERCENT_VALUE
|
||||||
|
val yAirUtil = height - (airUtilRatio * height)
|
||||||
|
drawCircle(
|
||||||
|
color = DEVICE_METRICS_COLORS[Device.AIR_UTIL.ordinal],
|
||||||
|
radius = dataPointRadius,
|
||||||
|
center = Offset(x1, yAirUtil)
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Battery line */
|
||||||
|
val nextTelemetry = telemetries.getOrNull(i + 1) ?: telemetries.last()
|
||||||
|
val y1Ratio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE
|
||||||
|
val y1 = height - (y1Ratio * height)
|
||||||
|
|
||||||
|
val x2Ratio = (nextTelemetry.time - oldest.time).toFloat() / timeDiff
|
||||||
|
val x2 = x2Ratio * width
|
||||||
|
|
||||||
|
val y2Ratio = nextTelemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE
|
||||||
|
val y2 = height - (y2Ratio * height)
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
moveTo(x1, y1)
|
||||||
|
}
|
||||||
|
|
||||||
|
quadraticTo(x1, y1, (x1 + x2) / 2f, (y1 + y2) / 2f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Battery Line */
|
||||||
|
drawPath(
|
||||||
|
path = strokePath,
|
||||||
|
color = DEVICE_METRICS_COLORS[Device.BATTERY.ordinal],
|
||||||
|
style = Stroke(
|
||||||
|
width = dataPointRadius,
|
||||||
|
cap = StrokeCap.Round
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
YAxisLabels(
|
||||||
|
modifier = modifier.weight(weight = .1f),
|
||||||
graphColor,
|
graphColor,
|
||||||
lineColors = listOf(graphColor, Orange, Color.Red, graphColor, graphColor),
|
|
||||||
minValue = 0f,
|
minValue = 0f,
|
||||||
maxValue = 100f
|
maxValue = 100f
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Plot Battery Line, ChUtil, and AirUtilTx */
|
|
||||||
Canvas(modifier = modifier) {
|
|
||||||
|
|
||||||
val height = size.height
|
|
||||||
val width = size.width - 28.dp.toPx()
|
|
||||||
val spacePerEntry = (width - spacing) / telemetries.size
|
|
||||||
val dataPointRadius = 2.dp.toPx()
|
|
||||||
var lastX: Float
|
|
||||||
val strokePath = Path().apply {
|
|
||||||
for (i in telemetries.indices) {
|
|
||||||
val telemetry = telemetries[i]
|
|
||||||
val nextTelemetry = telemetries.getOrNull(i + 1) ?: telemetries.last()
|
|
||||||
val leftRatio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE
|
|
||||||
val rightRatio = nextTelemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE
|
|
||||||
|
|
||||||
val x1 = spacing + i * spacePerEntry
|
|
||||||
val y1 = height - (leftRatio * height)
|
|
||||||
|
|
||||||
/* Channel Utilization */
|
|
||||||
val chUtilRatio = telemetry.deviceMetrics.channelUtilization / MAX_PERCENT_VALUE
|
|
||||||
val yChUtil = height - (chUtilRatio * height)
|
|
||||||
drawCircle(
|
|
||||||
color = DEVICE_METRICS_COLORS[Device.CH_UTIL.ordinal],
|
|
||||||
radius = dataPointRadius,
|
|
||||||
center = Offset(x1, yChUtil)
|
|
||||||
)
|
|
||||||
|
|
||||||
/* Air Utilization Transmit */
|
|
||||||
val airUtilRatio = telemetry.deviceMetrics.airUtilTx / MAX_PERCENT_VALUE
|
|
||||||
val yAirUtil = height - (airUtilRatio * height)
|
|
||||||
drawCircle(
|
|
||||||
color = DEVICE_METRICS_COLORS[Device.AIR_UTIL.ordinal],
|
|
||||||
radius = dataPointRadius,
|
|
||||||
center = Offset(x1, yAirUtil)
|
|
||||||
)
|
|
||||||
|
|
||||||
val x2 = spacing + (i + 1) * spacePerEntry
|
|
||||||
val y2 = height - (rightRatio * height)
|
|
||||||
if (i == 0) {
|
|
||||||
moveTo(x1, y1)
|
|
||||||
}
|
|
||||||
|
|
||||||
lastX = (x1 + x2) / 2f
|
|
||||||
|
|
||||||
quadraticTo(x1, y1, lastX, (y1 + y2) / 2f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Battery Line */
|
|
||||||
drawPath(
|
|
||||||
path = strokePath,
|
|
||||||
color = DEVICE_METRICS_COLORS[Device.BATTERY.ordinal],
|
|
||||||
style = Stroke(
|
|
||||||
width = dataPointRadius,
|
|
||||||
cap = StrokeCap.Round
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
@ -246,7 +289,7 @@ private fun DeviceMetricsCard(telemetry: Telemetry) {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = TIME_FORMAT.format(time),
|
text = DATE_TIME_FORMAT.format(time),
|
||||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||||
fontSize = MaterialTheme.typography.button.fontSize
|
fontSize = MaterialTheme.typography.button.fontSize
|
||||||
)
|
)
|
||||||
|
|
|
@ -63,7 +63,7 @@ import com.geeksville.mesh.copy
|
||||||
import com.geeksville.mesh.model.MetricsViewModel
|
import com.geeksville.mesh.model.MetricsViewModel
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.X_AXIS_SPACING
|
import com.geeksville.mesh.ui.components.CommonCharts.X_AXIS_SPACING
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.TIME_FORMAT
|
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||||
|
|
||||||
private val ENVIRONMENT_METRICS_COLORS = listOf(Color.Red, Color.Blue, Color.Green)
|
private val ENVIRONMENT_METRICS_COLORS = listOf(Color.Red, Color.Blue, Color.Green)
|
||||||
private enum class Environment {
|
private enum class Environment {
|
||||||
|
@ -170,8 +170,8 @@ private fun EnvironmentMetricsChart(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
TimeLabels(
|
TimeLabels(
|
||||||
oldest = telemetries.first().time * MS_PER_SEC,
|
oldest = telemetries.first().time,
|
||||||
newest = telemetries.last().time * MS_PER_SEC
|
newest = telemetries.last().time
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
@ -428,7 +428,7 @@ private fun EnvironmentMetricsCard(telemetry: Telemetry, environmentDisplayFahre
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = TIME_FORMAT.format(time),
|
text = DATE_TIME_FORMAT.format(time),
|
||||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||||
fontSize = MaterialTheme.typography.button.fontSize
|
fontSize = MaterialTheme.typography.button.fontSize
|
||||||
)
|
)
|
||||||
|
|
|
@ -64,7 +64,7 @@ import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.LINE_LIMIT
|
import com.geeksville.mesh.ui.components.CommonCharts.LINE_LIMIT
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.TEXT_PAINT_ALPHA
|
import com.geeksville.mesh.ui.components.CommonCharts.TEXT_PAINT_ALPHA
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.LEFT_LABEL_SPACING
|
import com.geeksville.mesh.ui.components.CommonCharts.LEFT_LABEL_SPACING
|
||||||
import com.geeksville.mesh.ui.components.CommonCharts.TIME_FORMAT
|
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||||
|
|
||||||
private val METRICS_COLORS = listOf(Color.Green, Color.Blue)
|
private val METRICS_COLORS = listOf(Color.Green, Color.Blue)
|
||||||
|
|
||||||
|
@ -139,8 +139,8 @@ private fun SignalMetricsChart(
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeLabels(
|
TimeLabels(
|
||||||
oldest = meshPackets.first().rxTime * MS_PER_SEC,
|
oldest = meshPackets.first().rxTime,
|
||||||
newest = meshPackets.last().rxTime * MS_PER_SEC
|
newest = meshPackets.last().rxTime
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
@ -274,7 +274,7 @@ private fun SignalMetricsCard(meshPacket: MeshPacket) {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = TIME_FORMAT.format(time),
|
text = DATE_TIME_FORMAT.format(time),
|
||||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||||
fontSize = MaterialTheme.typography.button.fontSize
|
fontSize = MaterialTheme.typography.button.fontSize
|
||||||
)
|
)
|
||||||
|
|
Ładowanie…
Reference in New Issue