feat: add multi-parameter chart view with graph icon
This commit is contained in:
@@ -2,6 +2,7 @@ package com.example.esp32aldldashboard
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.example.esp32aldldashboard.bluetooth.BluetoothService
|
import com.example.esp32aldldashboard.bluetooth.BluetoothService
|
||||||
|
import com.example.esp32aldldashboard.repository.ChartPreferencesRepository
|
||||||
import com.example.esp32aldldashboard.repository.SettingsRepository
|
import com.example.esp32aldldashboard.repository.SettingsRepository
|
||||||
import com.example.esp32aldldashboard.repository.TelemetryRepository
|
import com.example.esp32aldldashboard.repository.TelemetryRepository
|
||||||
import com.example.esp32aldldashboard.logging.CsvLogger
|
import com.example.esp32aldldashboard.logging.CsvLogger
|
||||||
@@ -15,11 +16,13 @@ class AldlApplication : Application() {
|
|||||||
|
|
||||||
lateinit var csvLogger: CsvLogger
|
lateinit var csvLogger: CsvLogger
|
||||||
lateinit var rawStreamLogger: RawStreamLogger
|
lateinit var rawStreamLogger: RawStreamLogger
|
||||||
|
lateinit var chartPreferencesRepository: ChartPreferencesRepository
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
val database = TelemetryDatabase.getDatabase(this)
|
val database = TelemetryDatabase.getDatabase(this)
|
||||||
settingsRepository = SettingsRepository(this)
|
settingsRepository = SettingsRepository(this)
|
||||||
|
chartPreferencesRepository = ChartPreferencesRepository(this)
|
||||||
csvLogger = CsvLogger(this)
|
csvLogger = CsvLogger(this)
|
||||||
rawStreamLogger = RawStreamLogger(this)
|
rawStreamLogger = RawStreamLogger(this)
|
||||||
bluetoothService = BluetoothService(this, rawStreamLogger, settingsRepository)
|
bluetoothService = BluetoothService(this, rawStreamLogger, settingsRepository)
|
||||||
|
|||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
package com.example.esp32aldldashboard.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.*
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import com.example.esp32aldldashboard.ui.charts.ChartParameter
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
val Context.chartPreferencesDataStore: DataStore<Preferences> by preferencesDataStore(name = "chart_preferences")
|
||||||
|
|
||||||
|
class ChartPreferencesRepository(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val CHART_VIEW_MODE = stringPreferencesKey("chart_view_mode")
|
||||||
|
val SELECTED_PARAMETERS = stringSetPreferencesKey("selected_parameters")
|
||||||
|
val SINGLE_CHART_PARAMETER = stringPreferencesKey("single_chart_parameter")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ViewMode {
|
||||||
|
SINGLE, MULTI
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewModeFlow: Flow<ViewMode> = context.chartPreferencesDataStore.data
|
||||||
|
.map { preferences ->
|
||||||
|
val modeString = preferences[CHART_VIEW_MODE] ?: ViewMode.MULTI.name
|
||||||
|
try {
|
||||||
|
ViewMode.valueOf(modeString)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
ViewMode.MULTI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectedParametersFlow: Flow<Set<ChartParameter>> = context.chartPreferencesDataStore.data
|
||||||
|
.map { preferences ->
|
||||||
|
val paramNames = preferences[SELECTED_PARAMETERS] ?: setOf(
|
||||||
|
ChartParameter.RPM.name,
|
||||||
|
ChartParameter.MAP.name,
|
||||||
|
ChartParameter.TPS.name,
|
||||||
|
ChartParameter.O2_SENSOR.name
|
||||||
|
)
|
||||||
|
paramNames.mapNotNull { name ->
|
||||||
|
try {
|
||||||
|
ChartParameter.valueOf(name)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
val singleChartParameterFlow: Flow<ChartParameter> = context.chartPreferencesDataStore.data
|
||||||
|
.map { preferences ->
|
||||||
|
val paramName = preferences[SINGLE_CHART_PARAMETER] ?: ChartParameter.RPM.name
|
||||||
|
try {
|
||||||
|
ChartParameter.valueOf(paramName)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
ChartParameter.RPM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setViewMode(mode: ViewMode) {
|
||||||
|
context.chartPreferencesDataStore.edit { preferences ->
|
||||||
|
preferences[CHART_VIEW_MODE] = mode.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setSelectedParameters(parameters: Set<ChartParameter>) {
|
||||||
|
context.chartPreferencesDataStore.edit { preferences ->
|
||||||
|
preferences[SELECTED_PARAMETERS] = parameters.map { it.name }.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setSingleChartParameter(parameter: ChartParameter) {
|
||||||
|
context.chartPreferencesDataStore.edit { preferences ->
|
||||||
|
preferences[SINGLE_CHART_PARAMETER] = parameter.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun toggleParameter(parameter: ChartParameter) {
|
||||||
|
context.chartPreferencesDataStore.edit { preferences ->
|
||||||
|
val current = preferences[SELECTED_PARAMETERS] ?: setOf(
|
||||||
|
ChartParameter.RPM.name,
|
||||||
|
ChartParameter.MAP.name,
|
||||||
|
ChartParameter.TPS.name,
|
||||||
|
ChartParameter.O2_SENSOR.name
|
||||||
|
)
|
||||||
|
val updated = if (current.contains(parameter.name)) {
|
||||||
|
current - parameter.name
|
||||||
|
} else {
|
||||||
|
current + parameter.name
|
||||||
|
}
|
||||||
|
preferences[SELECTED_PARAMETERS] = updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.example.esp32aldldashboard.ui.charts
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.example.esp32aldldashboard.parser.ALDLFrame
|
||||||
|
|
||||||
|
enum class ChartParameter(
|
||||||
|
val displayName: String,
|
||||||
|
val color: Color,
|
||||||
|
val maxValue: Float,
|
||||||
|
val extractValue: (ALDLFrame) -> Float
|
||||||
|
) {
|
||||||
|
RPM(
|
||||||
|
displayName = "RPM",
|
||||||
|
color = Color(0xFF00FFCC),
|
||||||
|
maxValue = 6000f,
|
||||||
|
extractValue = { it.engineSpeedRpm.toFloat() }
|
||||||
|
),
|
||||||
|
COOLANT_TEMP(
|
||||||
|
displayName = "Coolant Temp",
|
||||||
|
color = Color(0xFFFF5722),
|
||||||
|
maxValue = 250f,
|
||||||
|
extractValue = { it.coolantTempC }
|
||||||
|
),
|
||||||
|
MAP(
|
||||||
|
displayName = "MAP",
|
||||||
|
color = Color(0xFF2196F3),
|
||||||
|
maxValue = 105f,
|
||||||
|
extractValue = { it.mapKpa }
|
||||||
|
),
|
||||||
|
TPS(
|
||||||
|
displayName = "TPS",
|
||||||
|
color = Color(0xFF9C27B0),
|
||||||
|
maxValue = 5.5f,
|
||||||
|
extractValue = { it.tpsVolts }
|
||||||
|
),
|
||||||
|
O2_SENSOR(
|
||||||
|
displayName = "O2 Sensor",
|
||||||
|
color = Color(0xFF4CAF50),
|
||||||
|
maxValue = 1000f,
|
||||||
|
extractValue = { it.o2SensorMv }
|
||||||
|
),
|
||||||
|
BATTERY(
|
||||||
|
displayName = "Battery",
|
||||||
|
color = Color(0xFFFFEB3B),
|
||||||
|
maxValue = 16f,
|
||||||
|
extractValue = { it.batteryVolts }
|
||||||
|
),
|
||||||
|
SPARK_ADVANCE(
|
||||||
|
displayName = "Spark Advance",
|
||||||
|
color = Color(0xFF00BCD4),
|
||||||
|
maxValue = 40f,
|
||||||
|
extractValue = { it.sparkAdvance }
|
||||||
|
),
|
||||||
|
BPW(
|
||||||
|
displayName = "BPW",
|
||||||
|
color = Color(0xFFE91E63),
|
||||||
|
maxValue = 15f,
|
||||||
|
extractValue = { it.bpwMs }
|
||||||
|
),
|
||||||
|
MAT(
|
||||||
|
displayName = "MAT",
|
||||||
|
color = Color(0xFF795548),
|
||||||
|
maxValue = 80f,
|
||||||
|
extractValue = { it.matC }
|
||||||
|
),
|
||||||
|
BLM(
|
||||||
|
displayName = "BLM",
|
||||||
|
color = Color(0xFF3F51B5),
|
||||||
|
maxValue = 160f,
|
||||||
|
extractValue = { it.blm.toFloat() }
|
||||||
|
),
|
||||||
|
INTEGRATOR(
|
||||||
|
displayName = "Integrator",
|
||||||
|
color = Color(0xFF607D8B),
|
||||||
|
maxValue = 255f,
|
||||||
|
extractValue = { it.integrator.toFloat() }
|
||||||
|
),
|
||||||
|
IAC(
|
||||||
|
displayName = "IAC Position",
|
||||||
|
color = Color(0xFFFF9800),
|
||||||
|
maxValue = 255f,
|
||||||
|
extractValue = { it.iacPosition.toFloat() }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,8 +2,11 @@ package com.example.esp32aldldashboard.ui.charts
|
|||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -12,66 +15,308 @@ import androidx.compose.ui.graphics.drawscope.Stroke
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.example.esp32aldldashboard.parser.ALDLFrame
|
import com.example.esp32aldldashboard.parser.ALDLFrame
|
||||||
|
import com.example.esp32aldldashboard.repository.ChartPreferencesRepository
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChartsScreen(
|
fun ChartsScreen(
|
||||||
latestFrameFlow: StateFlow<ALDLFrame?>,
|
latestFrameFlow: StateFlow<ALDLFrame?>,
|
||||||
|
chartPreferencesRepository: ChartPreferencesRepository,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val latestFrame by latestFrameFlow.collectAsStateWithLifecycle()
|
val latestFrame by latestFrameFlow.collectAsStateWithLifecycle()
|
||||||
|
val viewMode by chartPreferencesRepository.viewModeFlow.collectAsStateWithLifecycle(
|
||||||
// We maintain a limited rolling history of points (e.g. 100 points)
|
initialValue = ChartPreferencesRepository.ViewMode.MULTI
|
||||||
val rpmHistory = remember { mutableStateListOf<Float>() }
|
)
|
||||||
|
val selectedParameters by chartPreferencesRepository.selectedParametersFlow.collectAsStateWithLifecycle(
|
||||||
|
initialValue = setOf(ChartParameter.RPM, ChartParameter.MAP, ChartParameter.TPS, ChartParameter.O2_SENSOR)
|
||||||
|
)
|
||||||
|
val singleChartParameter by chartPreferencesRepository.singleChartParameterFlow.collectAsStateWithLifecycle(
|
||||||
|
initialValue = ChartParameter.RPM
|
||||||
|
)
|
||||||
|
|
||||||
|
// History storage for all parameters
|
||||||
val maxHistorySize = 100
|
val maxHistorySize = 100
|
||||||
|
val histories = remember {
|
||||||
|
mutableStateMapOf<ChartParameter, MutableList<Float>>().apply {
|
||||||
|
ChartParameter.values().forEach { put(it, mutableStateListOf()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(latestFrame) {
|
LaunchedEffect(latestFrame) {
|
||||||
latestFrame?.let {
|
latestFrame?.let { frame ->
|
||||||
rpmHistory.add(it.engineSpeedRpm.toFloat())
|
ChartParameter.values().forEach { param ->
|
||||||
if (rpmHistory.size > maxHistorySize) {
|
val history = histories.getOrPut(param) { mutableStateListOf() }
|
||||||
rpmHistory.removeAt(0)
|
history.add(param.extractValue(frame))
|
||||||
|
if (history.size > maxHistorySize) {
|
||||||
|
history.removeAt(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxSize().padding(16.dp)) {
|
Column(modifier = modifier.fillMaxSize().padding(16.dp)) {
|
||||||
Text(
|
// Header with view mode toggle
|
||||||
text = "Real-Time Telemetry",
|
Row(
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
)
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Real-Time Telemetry",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
|
||||||
|
// View mode toggle
|
||||||
|
Row {
|
||||||
|
FilterChip(
|
||||||
|
selected = viewMode == ChartPreferencesRepository.ViewMode.SINGLE,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
chartPreferencesRepository.setViewMode(
|
||||||
|
if (viewMode == ChartPreferencesRepository.ViewMode.SINGLE)
|
||||||
|
ChartPreferencesRepository.ViewMode.MULTI
|
||||||
|
else
|
||||||
|
ChartPreferencesRepository.ViewMode.SINGLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text(if (viewMode == ChartPreferencesRepository.ViewMode.SINGLE) "Single" else "Multi") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
when (viewMode) {
|
||||||
|
ChartPreferencesRepository.ViewMode.SINGLE -> {
|
||||||
|
// Single chart mode
|
||||||
|
SingleChartView(
|
||||||
|
selectedParameter = singleChartParameter,
|
||||||
|
history = histories[singleChartParameter] ?: emptyList(),
|
||||||
|
onParameterChange = { param ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
chartPreferencesRepository.setSingleChartParameter(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ChartPreferencesRepository.ViewMode.MULTI -> {
|
||||||
|
// Multi chart mode
|
||||||
|
MultiChartView(
|
||||||
|
selectedParameters = selectedParameters,
|
||||||
|
histories = histories,
|
||||||
|
onToggleParameter = { param ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
chartPreferencesRepository.toggleParameter(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SingleChartView(
|
||||||
|
selectedParameter: ChartParameter,
|
||||||
|
history: List<Float>,
|
||||||
|
onParameterChange: (ChartParameter) -> Unit
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
// Parameter selector dropdown
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { expanded = it }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = selectedParameter.displayName,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Parameter") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
|
modifier = Modifier.menuAnchor().fillMaxWidth()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
ChartParameter.values().forEach { param ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(param.displayName) },
|
||||||
|
onClick = {
|
||||||
|
onParameterChange(param)
|
||||||
|
expanded = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Large single chart
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth().height(200.dp),
|
modifier = Modifier.fillMaxWidth().height(300.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E))
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E))
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Text(text = "RPM", color = Color(0xFF00FFCC))
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = selectedParameter.displayName,
|
||||||
|
color = selectedParameter.color,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
if (history.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = String.format("%.1f", history.last()),
|
||||||
|
color = selectedParameter.color,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
LineChart(
|
LineChart(
|
||||||
data = rpmHistory,
|
data = history,
|
||||||
maxValue = 6000f,
|
maxValue = selectedParameter.maxValue,
|
||||||
lineColor = Color(0xFF00FFCC),
|
lineColor = selectedParameter.color,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MultiChartView(
|
||||||
|
selectedParameters: Set<ChartParameter>,
|
||||||
|
histories: Map<ChartParameter, List<Float>>,
|
||||||
|
onToggleParameter: (ChartParameter) -> Unit
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
// Parameter toggle chips
|
||||||
|
LazyRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(ChartParameter.values().toList()) { param ->
|
||||||
|
val isSelected = selectedParameters.contains(param)
|
||||||
|
FilterChip(
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = { onToggleParameter(param) },
|
||||||
|
label = { Text(param.displayName) },
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = param.color.copy(alpha = 0.3f),
|
||||||
|
selectedLabelColor = param.color
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Additional charts can go here (e.g., O2, TPS)
|
// 2x2 grid of charts (up to 4)
|
||||||
|
val activeParams = selectedParameters.take(4)
|
||||||
|
|
||||||
|
if (activeParams.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(200.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Select parameters above to display charts",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
activeParams.chunked(2).forEach { rowParams ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
rowParams.forEach { param ->
|
||||||
|
val history = histories[param] ?: emptyList()
|
||||||
|
ChartCard(
|
||||||
|
parameter = param,
|
||||||
|
history = history,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Fill remaining space if odd number
|
||||||
|
if (rowParams.size == 1) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LineChart(
|
private fun ChartCard(
|
||||||
|
parameter: ChartParameter,
|
||||||
|
history: List<Float>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier.height(180.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E))
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = parameter.displayName,
|
||||||
|
color = parameter.color,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
if (history.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = String.format("%.1f", history.last()),
|
||||||
|
color = parameter.color,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
LineChart(
|
||||||
|
data = history,
|
||||||
|
maxValue = parameter.maxValue,
|
||||||
|
lineColor = parameter.color,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LineChart(
|
||||||
data: List<Float>,
|
data: List<Float>,
|
||||||
maxValue: Float,
|
maxValue: Float,
|
||||||
lineColor: Color,
|
lineColor: Color,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (data.isEmpty()) return
|
if (data.isEmpty()) {
|
||||||
|
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
||||||
|
Text(
|
||||||
|
text = "Waiting for data...",
|
||||||
|
color = Color.Gray,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Canvas(modifier = modifier) {
|
Canvas(modifier = modifier) {
|
||||||
val width = size.width
|
val width = size.width
|
||||||
@@ -94,7 +339,7 @@ fun LineChart(
|
|||||||
drawPath(
|
drawPath(
|
||||||
path = path,
|
path = path,
|
||||||
color = lineColor,
|
color = lineColor,
|
||||||
style = Stroke(width = 4.dp.toPx())
|
style = Stroke(width = 3.dp.toPx())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Build
|
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.filled.ShowChart
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -89,7 +89,7 @@ fun MainScreen(
|
|||||||
colors = NavigationBarItemDefaults.colors(selectedIconColor = NeonCyan, unselectedIconColor = TextMuted)
|
colors = NavigationBarItemDefaults.colors(selectedIconColor = NeonCyan, unselectedIconColor = TextMuted)
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
icon = { Icon(Icons.Default.Build, contentDescription = "Charts") },
|
icon = { Icon(Icons.Default.ShowChart, contentDescription = "Charts") },
|
||||||
label = { Text("Charts") },
|
label = { Text("Charts") },
|
||||||
selected = selectedTab == 1,
|
selected = selectedTab == 1,
|
||||||
onClick = { selectedTab = 1 },
|
onClick = { selectedTab = 1 },
|
||||||
@@ -117,6 +117,7 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
1 -> ChartsScreen(
|
1 -> ChartsScreen(
|
||||||
latestFrameFlow = viewModel.latestFrame,
|
latestFrameFlow = viewModel.latestFrame,
|
||||||
|
chartPreferencesRepository = app.chartPreferencesRepository,
|
||||||
modifier = modifier.padding(paddingValues)
|
modifier = modifier.padding(paddingValues)
|
||||||
)
|
)
|
||||||
2 -> SettingsScreen(
|
2 -> SettingsScreen(
|
||||||
|
|||||||
Reference in New Issue
Block a user