feat: Updated the env metrics graph to use the latest graph feature (#1667)

refactor: removed unused constants and function
pull/1672/head
Robert-0410 2025-03-13 13:29:50 -07:00 zatwierdzone przez GitHub
rodzic 5846bf5ee4
commit 7189d44b9c
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
4 zmienionych plików z 183 dodań i 271 usunięć

Wyświetl plik

@ -57,21 +57,14 @@ 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.components.CommonCharts.LINE_LIMIT
import com.geeksville.mesh.ui.components.CommonCharts.TEXT_PAINT_ALPHA
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.MAX_PERCENT_VALUE
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
import java.text.DateFormat
object CommonCharts {
val DATE_TIME_FORMAT: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
const val X_AXIS_SPACING = 8f
const val LEFT_LABEL_SPACING = 36
const val MS_PER_SEC = 1000L
const val LINE_LIMIT = 4
const val TEXT_PAINT_ALPHA = 192
const val MAX_PERCENT_VALUE = 100f
val INFANTRY_BLUE = Color(75, 119, 190)
}
@ -81,6 +74,8 @@ private const val LINE_OFF = 20f
private val TIME_FORMAT: DateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM)
private val DATE_FORMAT: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
private const val DATE_Y = 32f
private const val LINE_LIMIT = 4
private const val TEXT_PAINT_ALPHA = 192
data class LegendData(val nameRes: Int, val color: Color, val isLine: Boolean = false)
@ -100,78 +95,10 @@ fun ChartHeader(amount: Int) {
}
}
/**
* Draws chart lines and labels with respect to the Y-axis range; defined by (`maxValue` - `minValue`).
*
* @param labelColor The color to be used for the Y labels.
* @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.
*/
@Deprecated("Will soon be replaced with YAxisLabels() and HorizontalLinesOverlay()", level = DeprecationLevel.WARNING)
@Composable
fun ChartOverlay(
modifier: Modifier,
labelColor: Color,
lineColors: List<Color>,
minValue: Float,
maxValue: Float,
leaveSpace: Boolean = false
) {
val range = maxValue - minValue
val verticalSpacing = range / LINE_LIMIT
val density = LocalDensity.current
Canvas(modifier = modifier) {
val lineStart = if (leaveSpace) LEFT_LABEL_SPACING.dp.toPx() else 0f
val height = size.height
val width = size.width - 28.dp.toPx()
/* 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
}
/* 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()}",
width + 4.dp.toPx(),
y + 4.dp.toPx(),
textPaint
)
label += verticalSpacing
}
}
}
}
/**
* Draws chart lines with respect to the Y-axis.
*
* @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.
*/
@Composable
fun HorizontalLinesOverlay(

Wyświetl plik

@ -158,8 +158,7 @@ private fun DeviceMetricsChart(
val graphColor = MaterialTheme.colors.onSurface
val scrollState = rememberScrollState()
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp
val screenWidth = LocalConfiguration.current.screenWidthDp
val dp by remember(key1 = selectedTime) {
mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong()))
}
@ -169,7 +168,7 @@ private fun DeviceMetricsChart(
contentAlignment = Alignment.TopStart,
modifier = Modifier
.horizontalScroll(state = scrollState, reverseScrolling = true)
.weight(1f)
.weight(weight = 1f)
) {
/*

Wyświetl plik

@ -18,6 +18,7 @@
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -31,6 +32,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
@ -44,13 +46,9 @@ 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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.asComposePath
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
@ -62,10 +60,11 @@ import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.TimeFrame
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.DATE_TIME_FORMAT
import com.geeksville.mesh.ui.components.CommonCharts.INFANTRY_BLUE
import com.geeksville.mesh.util.GraphUtil.createPath
import com.geeksville.mesh.util.GraphUtil.drawPathWithGradient
private enum class Environment(val color: Color) {
TEMPERATURE(Color.Red),
@ -135,6 +134,7 @@ fun EnvironmentMetricsScreen(
.fillMaxWidth()
.fillMaxHeight(fraction = 0.33f),
telemetries = processedTelemetries.reversed(),
selectedTimeFrame,
promptInfoDialog = { displayInfoDialog = true }
)
@ -165,32 +165,31 @@ fun EnvironmentMetricsScreen(
private fun EnvironmentMetricsChart(
modifier: Modifier = Modifier,
telemetries: List<Telemetry>,
selectedTime: TimeFrame,
promptInfoDialog: () -> Unit
) {
ChartHeader(amount = telemetries.size)
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(
oldest = telemetries.first().time,
newest = telemetries.last().time
oldest = oldest.time,
newest = newest.time
)
Spacer(modifier = Modifier.height(16.dp))
val graphColor = MaterialTheme.colors.onSurface
val transparentTemperatureColor = remember {
Environment.TEMPERATURE.color.copy(alpha = 0.5f)
}
val transparentHumidityColor = remember {
Environment.HUMIDITY.color.copy(alpha = 0.5f)
}
val transparentIAQColor = remember {
Environment.IAQ.color.copy(alpha = 0.5f)
}
val spacing = X_AXIS_SPACING
/* Since both temperature and humidity are being plotted we need a combined min and max. */
/* Grab the combined min and max for all data being plotted. */
val (minTemp, maxTemp) = remember(key1 = telemetries) {
Pair(
telemetries.minBy { it.environmentMetrics.temperature },
@ -221,182 +220,128 @@ private fun EnvironmentMetricsChart(
)
val diff = max - min
Box(contentAlignment = Alignment.TopStart) {
ChartOverlay(
modifier = modifier,
labelColor = graphColor,
lineColors = List(size = 5) { graphColor },
val scrollState = rememberScrollState()
val screenWidth = LocalConfiguration.current.screenWidthDp
val dp by remember(key1 = selectedTime) {
mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong()))
}
Row {
Box(
contentAlignment = Alignment.TopStart,
modifier = Modifier
.horizontalScroll(state = scrollState, reverseScrolling = true)
.weight(weight = 1f)
) {
HorizontalLinesOverlay(
modifier.width(dp),
lineColors = List(size = 5) { graphColor }
)
TimeAxisOverlay(
modifier = modifier.width(dp),
oldest = oldest.time,
newest = newest.time,
selectedTime.lineInterval()
)
Canvas(modifier = modifier.width(dp)) {
val height = size.height
val width = size.width
/* Temperature */
var index = 0
var first: Int
while (index < telemetries.size) {
first = index
val path = Path()
index = createPath(
telemetries = telemetries,
index = index,
path = path,
oldestTime = oldest.time,
timeRange = timeDiff,
width = width,
timeThreshold = selectedTime.timeThreshold()
) { i ->
val telemetry = telemetries.getOrNull(i) ?: telemetries.last()
val ratio = (telemetry.environmentMetrics.temperature - min) / diff
val y = height - (ratio * height)
return@createPath y
}
drawPathWithGradient(
path = path,
color = Environment.TEMPERATURE.color,
height = height,
x1 = ((telemetries[index - 1].time - oldest.time).toFloat() / timeDiff) * width,
x2 = ((telemetries[first].time - oldest.time).toFloat() / timeDiff) * width
)
}
/* Relative Humidity */
index = 0
while (index < telemetries.size) {
first = index
val path = Path()
index = createPath(
telemetries = telemetries,
index = index,
path = path,
oldestTime = oldest.time,
timeRange = timeDiff,
width = width,
timeThreshold = selectedTime.timeThreshold()
) { i ->
val telemetry = telemetries.getOrNull(i) ?: telemetries.last()
val ratio = (telemetry.environmentMetrics.relativeHumidity - min) / diff
val y = height - (ratio * height)
return@createPath y
}
drawPathWithGradient(
path = path,
color = Environment.HUMIDITY.color,
height = height,
x1 = ((telemetries[index - 1].time - oldest.time).toFloat() / timeDiff) * width,
x2 = ((telemetries[first].time - oldest.time).toFloat() / timeDiff) * width
)
}
/* Air Quality */
index = 0
while (index < telemetries.size) {
first = index
val path = Path()
index = createPath(
telemetries = telemetries,
index = index,
path = path,
oldestTime = oldest.time,
timeRange = timeDiff,
width = width,
timeThreshold = selectedTime.timeThreshold()
) { i ->
val telemetry = telemetries.getOrNull(i) ?: telemetries.last()
val ratio = (telemetry.environmentMetrics.iaq - min) / diff
val y = height - (ratio * height)
return@createPath y
}
drawPathWithGradient(
path = path,
color = Environment.IAQ.color,
height = height,
x1 = ((telemetries[index - 1].time - oldest.time).toFloat() / timeDiff) * width,
x2 = ((telemetries[first].time - oldest.time).toFloat() / timeDiff) * width
)
}
}
}
YAxisLabels(
modifier = modifier.weight(weight = .1f),
graphColor,
minValue = min,
maxValue = max
)
/* Plot Temperature and Relative Humidity */
Canvas(modifier = modifier) {
val height = size.height
val width = size.width - 28.dp.toPx()
val spacePerEntry = (width - spacing) / telemetries.size
/* Temperature */
var lastTempX = 0f
val temperaturePath = Path().apply {
for (i in telemetries.indices) {
val envMetrics = telemetries[i].environmentMetrics
val nextEnvMetrics =
(telemetries.getOrNull(i + 1) ?: telemetries.last()).environmentMetrics
val leftRatio = (envMetrics.temperature - min) / diff
val rightRatio = (nextEnvMetrics.temperature - min) / diff
val x1 = spacing + i * spacePerEntry
val y1 = height - (leftRatio * height)
val x2 = spacing + (i + 1) * spacePerEntry
val y2 = height - (rightRatio * height)
if (i == 0) {
moveTo(x1, y1)
}
lastTempX = (x1 + x2) / 2f
quadraticTo(
x1, y1, lastTempX, (y1 + y2) / 2f
)
}
}
val fillPath = android.graphics.Path(temperaturePath.asAndroidPath())
.asComposePath()
.apply {
lineTo(lastTempX, height)
lineTo(spacing, height)
close()
}
drawPath(
path = fillPath,
brush = Brush.verticalGradient(
colors = listOf(
transparentTemperatureColor,
Color.Transparent
),
endY = height
),
)
drawPath(
path = temperaturePath,
color = Environment.TEMPERATURE.color,
style = Stroke(
width = 2.dp.toPx(),
cap = StrokeCap.Round
)
)
/* Relative Humidity */
var lastHumidityX = 0f
val humidityPath = Path().apply {
for (i in telemetries.indices) {
val envMetrics = telemetries[i].environmentMetrics
val nextEnvMetrics =
(telemetries.getOrNull(i + 1) ?: telemetries.last()).environmentMetrics
val leftRatio = (envMetrics.relativeHumidity - min) / diff
val rightRatio = (nextEnvMetrics.relativeHumidity - min) / diff
val x1 = spacing + i * spacePerEntry
val y1 = height - (leftRatio * height)
val x2 = spacing + (i + 1) * spacePerEntry
val y2 = height - (rightRatio * height)
if (i == 0) {
moveTo(x1, y1)
}
lastHumidityX = (x1 + x2) / 2f
quadraticTo(
x1, y1, lastHumidityX, (y1 + y2) / 2f
)
}
}
val fillHumidityPath = android.graphics.Path(humidityPath.asAndroidPath())
.asComposePath()
.apply {
lineTo(lastHumidityX, height)
lineTo(spacing, height)
close()
}
drawPath(
path = fillHumidityPath,
brush = Brush.verticalGradient(
colors = listOf(
transparentHumidityColor,
Color.Transparent
),
endY = height
),
)
drawPath(
path = humidityPath,
color = Environment.HUMIDITY.color,
style = Stroke(
width = 2.dp.toPx(),
cap = StrokeCap.Round
)
)
/* Air Quality */
var lastIaqX = 0f
val iaqPath = Path().apply {
for (i in telemetries.indices) {
val envMetrics = telemetries[i].environmentMetrics
val nextEnvMetrics =
(telemetries.getOrNull(i + 1) ?: telemetries.last()).environmentMetrics
val leftRatio = (envMetrics.iaq - min) / diff
val rightRatio = (nextEnvMetrics.iaq - min) / diff
val x1 = spacing + i * spacePerEntry
val y1 = height - (leftRatio * height)
val x2 = spacing + (i + 1) * spacePerEntry
val y2 = height - (rightRatio * height)
if (i == 0) {
moveTo(x1, y1)
}
lastIaqX = (x1 + x2) / 2f
quadraticTo(
x1,
y1,
lastIaqX,
(y1 + y2) / 2f
)
}
}
val fillIaqPath = android.graphics.Path(iaqPath.asAndroidPath())
.asComposePath()
.apply {
lineTo(lastIaqX, height)
lineTo(spacing, height)
close()
}
drawPath(
path = fillIaqPath,
brush = Brush.verticalGradient(
colors = listOf(
transparentIAQColor,
Color.Transparent
),
endY = height
),
)
drawPath(
path = iaqPath,
color = Environment.IAQ.color,
style = Stroke(
width = 2.dp.toPx(),
cap = StrokeCap.Round
)
)
}
}
Spacer(modifier = Modifier.height(16.dp))

Wyświetl plik

@ -20,8 +20,15 @@ package com.geeksville.mesh.util
import android.content.res.Resources
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.asComposePath
import androidx.compose.ui.graphics.drawscope.DrawContext
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.TelemetryProtos.Telemetry
object GraphUtil {
@ -104,4 +111,38 @@ object GraphUtil {
}
return i
}
fun DrawScope.drawPathWithGradient(
path: Path,
color: Color,
height: Float,
x1: Float,
x2: Float
) {
drawPath(
path = path,
color = color,
style = Stroke(
width = 2.dp.toPx(),
cap = StrokeCap.Round
)
)
val fillPath = android.graphics.Path(path.asAndroidPath())
.asComposePath()
.apply {
lineTo(x1, height)
lineTo(x2, height)
close()
}
drawPath(
path = fillPath,
brush = Brush.verticalGradient(
colors = listOf(
color.copy(alpha = 0.5f),
Color.Transparent
),
endY = height
),
)
}
}