feat(stats): improve AC/DC ratio bar visualization

- Change colors: green for AC, yellow/amber for DC
- Make bar thicker (20dp height with rounded corners)
- Calculate ratios based on charged kWh instead of charge count
- Show energy values (kWh) in the stats row
- Show charge counts in small text below each bar side
- Remove percentage numbers from display

Added new DAO queries to sum energy by AC/DC type:
- sumAcChargeEnergy / sumAcChargeEnergyInRange
- sumDcChargeEnergy / sumDcChargeEnergyInRange

Added acChargeEnergyKwh and dcChargeEnergyKwh fields to DeepStats.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Davide Ferrari
2026-01-05 17:24:20 +01:00
co-authored by Claude Opus 4.5
parent 4a3e015af7
commit 49e8eb25f1
4 changed files with 100 additions and 25 deletions
@@ -279,6 +279,38 @@ interface AggregateDao {
@Query("SELECT chargeId FROM charge_detail_aggregates WHERE carId = :carId AND isFastCharger = 1")
suspend fun getDcChargeIds(carId: Int): List<Int>
// Sum of energy added for AC charges (join with summary to get energyAdded)
@Query("""
SELECT COALESCE(SUM(c.energyAdded), 0.0) FROM charge_detail_aggregates a
JOIN charges_summary c ON a.chargeId = c.chargeId
WHERE a.carId = :carId AND a.isFastCharger = 0
""")
suspend fun sumAcChargeEnergy(carId: Int): Double
@Query("""
SELECT COALESCE(SUM(c.energyAdded), 0.0) FROM charge_detail_aggregates a
JOIN charges_summary c ON a.chargeId = c.chargeId
WHERE a.carId = :carId AND a.isFastCharger = 0
AND c.startDate >= :startDate AND c.startDate < :endDate
""")
suspend fun sumAcChargeEnergyInRange(carId: Int, startDate: String, endDate: String): Double
// Sum of energy added for DC charges
@Query("""
SELECT COALESCE(SUM(c.energyAdded), 0.0) FROM charge_detail_aggregates a
JOIN charges_summary c ON a.chargeId = c.chargeId
WHERE a.carId = :carId AND a.isFastCharger = 1
""")
suspend fun sumDcChargeEnergy(carId: Int): Double
@Query("""
SELECT COALESCE(SUM(c.energyAdded), 0.0) FROM charge_detail_aggregates a
JOIN charges_summary c ON a.chargeId = c.chargeId
WHERE a.carId = :carId AND a.isFastCharger = 1
AND c.startDate >= :startDate AND c.startDate < :endDate
""")
suspend fun sumDcChargeEnergyInRange(carId: Int, startDate: String, endDate: String): Double
// Get all processed charge IDs (for checking if we have aggregate data)
@Query("SELECT chargeId FROM charge_detail_aggregates WHERE carId = :carId")
suspend fun getAllProcessedChargeIds(carId: Int): List<Int>
@@ -238,6 +238,8 @@ class StatsRepository @Inject constructor(
acChargeCount = aggregateDao.countAcCharges(carId),
dcChargeCount = aggregateDao.countDcCharges(carId),
acChargeEnergyKwh = aggregateDao.sumAcChargeEnergy(carId),
dcChargeEnergyKwh = aggregateDao.sumDcChargeEnergy(carId),
driveDetailsProcessed = driveCount,
chargeDetailsProcessed = chargeCount
@@ -341,6 +343,8 @@ class StatsRepository @Inject constructor(
acChargeCount = aggregateDao.countAcChargesInRange(carId, startDate, endDate),
dcChargeCount = aggregateDao.countDcChargesInRange(carId, startDate, endDate),
acChargeEnergyKwh = aggregateDao.sumAcChargeEnergyInRange(carId, startDate, endDate),
dcChargeEnergyKwh = aggregateDao.sumDcChargeEnergyInRange(carId, startDate, endDate),
driveDetailsProcessed = driveCount,
chargeDetailsProcessed = chargeCount
@@ -98,6 +98,8 @@ data class DeepStats(
// === AC/DC Ratio ===
val acChargeCount: Int,
val dcChargeCount: Int,
val acChargeEnergyKwh: Double,
val dcChargeEnergyKwh: Double,
// === Sync Progress ===
val driveDetailsProcessed: Int,
@@ -1,8 +1,10 @@
package com.matedroid.ui.screens.stats
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -16,6 +18,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
@@ -46,6 +49,8 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.foundation.lazy.LazyColumn as LogLazyColumn
import androidx.compose.foundation.lazy.items as logItems
import androidx.compose.foundation.rememberScrollState
@@ -714,53 +719,85 @@ private fun ChargingPowerCard(deepStats: DeepStats, palette: CarColorPalette) {
@Composable
private fun AcDcRatioCard(deepStats: DeepStats, palette: CarColorPalette) {
val total = deepStats.acChargeCount + deepStats.dcChargeCount
if (total == 0) {
val totalEnergy = deepStats.acChargeEnergyKwh + deepStats.dcChargeEnergyKwh
if (totalEnergy <= 0) {
return // No charge data
}
val acPercent = (deepStats.acChargeCount * 100f / total).toInt()
val dcPercent = (deepStats.dcChargeCount * 100f / total).toInt()
val acRatio = (deepStats.acChargeEnergyKwh / totalEnergy).toFloat()
val acColor = Color(0xFF4CAF50) // Green
val dcColor = Color(0xFFFFC107) // Yellow/Amber
StatsCard(
title = "AC/DC Charging Ratio",
icon = Icons.Default.BatteryChargingFull,
palette = palette
) {
// Energy stats row
Row(modifier = Modifier.fillMaxWidth()) {
StatItem(
label = "AC Charges",
value = "${deepStats.acChargeCount} ($acPercent%)",
label = "AC Energy",
value = "%.1f kWh".format(deepStats.acChargeEnergyKwh),
modifier = Modifier.weight(1f)
)
StatItem(
label = "DC Charges",
value = "${deepStats.dcChargeCount} ($dcPercent%)",
label = "DC Energy",
value = "%.1f kWh".format(deepStats.dcChargeEnergyKwh),
modifier = Modifier.weight(1f)
)
}
Spacer(modifier = Modifier.height(12.dp))
// Progress bar showing ratio
LinearProgressIndicator(
progress = { acPercent / 100f },
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.tertiary
)
// Custom ratio bar (thicker, green/yellow)
Box(
modifier = Modifier
.fillMaxWidth()
.height(20.dp)
.clip(RoundedCornerShape(10.dp))
.background(dcColor)
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(acRatio)
.background(acColor)
)
}
Spacer(modifier = Modifier.height(4.dp))
// Labels with counts below
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "AC (Home/Destination)",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "DC (Supercharger)",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.tertiary
)
Column {
Text(
text = "AC",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = acColor
)
Text(
text = "${deepStats.acChargeCount} charges",
style = MaterialTheme.typography.labelSmall,
color = palette.onSurfaceVariant
)
}
Column(horizontalAlignment = Alignment.End) {
Text(
text = "DC",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = dcColor
)
Text(
text = "${deepStats.dcChargeCount} charges",
style = MaterialTheme.typography.labelSmall,
color = palette.onSurfaceVariant
)
}
}
}
}