diff --git a/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt b/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt index a68b4c62..89cab432 100644 --- a/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt @@ -834,8 +834,12 @@ private fun ChargesChartPage( ChargesChartType.COUNT -> { v -> v.toInt().toString() } } - // Show max ~6 labels to avoid crowding - val labelInterval = ((barData.size + 5) / 6).coerceAtLeast(1) + // Set number of labels to display + val labelInterval = when { + barData.size <= 7 -> 1 // Show all for Today and last 7 days + barData.size <= 30 -> 3 // Show 1 label every 3 bars for last 30 days + else -> ((barData.size + 5) / 6).coerceAtLeast(1) + } InteractiveBarChart( data = barData, diff --git a/app/src/main/java/com/matedroid/ui/screens/charges/ChargesViewModel.kt b/app/src/main/java/com/matedroid/ui/screens/charges/ChargesViewModel.kt index 5dbd7c80..15bd21fa 100644 --- a/app/src/main/java/com/matedroid/ui/screens/charges/ChargesViewModel.kt +++ b/app/src/main/java/com/matedroid/ui/screens/charges/ChargesViewModel.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.time.LocalDate +import java.time.LocalDateTime import java.time.YearMonth import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit @@ -125,16 +126,17 @@ class ChargesViewModel @Inject constructor( fun setDateFilter(filter: DateFilter) { val endDate = LocalDate.now() - val startDate = filter.days?.let { endDate.minusDays(it) } - _uiState.update { it.copy(startDate = startDate, endDate = if (filter.days != null) endDate else null, selectedFilter = filter) } + val startDate = filter.days?.let { days -> + if (days > 0) endDate.minusDays(days - 1) else endDate + } + _uiState.update { it.copy( + selectedFilter = filter, + startDate = startDate, + endDate = if (filter.days != null) endDate else null + )} loadCharges(startDate, if (filter.days != null) endDate else null) } - fun clearDateFilter() { - _uiState.update { it.copy(startDate = null, endDate = null, selectedFilter = DateFilter.ALL_TIME) } - loadCharges(null, null) - } - fun refresh() { carId?.let { _uiState.update { it.copy(isRefreshing = true) } @@ -252,7 +254,7 @@ class ChargesViewModel @Inject constructor( // Calculate summary and chart data from filtered charges val summary = calculateSummary(chargesForStats) - val chartData = calculateChartData(chargesForStats, granularity) + val chartData = calculateChartData(chargesForStats, granularity, state.startDate) _uiState.update { it.copy( @@ -275,51 +277,78 @@ class ChargesViewModel @Inject constructor( } } - private fun calculateChartData(charges: List, granularity: ChartGranularity): List { + private fun calculateChartData(charges: List, granularity: ChartGranularity, startDate: LocalDate?): List { if (charges.isEmpty()) return emptyList() val formatter = DateTimeFormatter.ISO_DATE_TIME val weekFields = WeekFields.of(Locale.getDefault()) - return charges - .mapNotNull { charge -> + // Group the charges by day + val chargesByDay = charges.mapNotNull { charge -> + charge.startDate?.let { + try { + // Use of localdatetime to support the full ISO format + val date = LocalDateTime.parse(it, formatter).toLocalDate() + date.toEpochDay() to charge + } catch (e: Exception) { null } + } + }.groupBy({ it.first }, { it.second }) + + return if (granularity == ChartGranularity.DAILY) { + // DAILY ranges (today, last 7 and last 30 days + // If not startDate (All Time), get the first trip, or today + val start = startDate ?: (chargesByDay.keys.minOrNull()?.let { LocalDate.ofEpochDay(it) } ?: LocalDate.now()) + val end = LocalDate.now() + val result = mutableListOf() + var current = start + while (!current.isAfter(end)) { + val key = current.toEpochDay() + val itemsInDay = chargesByDay[key] ?: emptyList() + result.add( + createChargeChartPoint( + label = current.format(DateTimeFormatter.ofPattern("d/M")), + sortKey = key, + charges = itemsInDay + ) + ) + current = current.plusDays(1) + } + result + } else { + // WEEKLY and MONTHLY ranges + charges.mapNotNull { charge -> charge.startDate?.let { dateStr -> try { - val date = LocalDate.parse(dateStr, formatter) + val date = LocalDateTime.parse(dateStr, formatter).toLocalDate() val (label, sortKey) = when (granularity) { - ChartGranularity.DAILY -> { - val dayLabel = date.format(DateTimeFormatter.ofPattern("d/M")) - dayLabel to date.toEpochDay() - } ChartGranularity.WEEKLY -> { - val weekOfYear = date.get(weekFields.weekOfWeekBasedYear()) - val year = date.get(weekFields.weekBasedYear()) - "W$weekOfYear" to (year * 100L + weekOfYear) + val firstDay = date.with(weekFields.dayOfWeek(), 1) + "W${date.get(weekFields.weekOfYear())}" to firstDay.toEpochDay() } - ChartGranularity.MONTHLY -> { - val yearMonth = YearMonth.of(date.year, date.month) - yearMonth.format(DateTimeFormatter.ofPattern("MMM yy")) to (date.year * 12L + date.monthValue) + else -> { // MONTHLY + date.format(DateTimeFormatter.ofPattern("MMM yy")) to YearMonth.from(date).atDay(1).toEpochDay() } } Triple(label, sortKey, charge) - } catch (e: Exception) { - null - } + } catch (e: Exception) { null } } } - .groupBy { Pair(it.first, it.second) } - .map { (key, chargesInPeriod) -> - ChargeChartData( - label = key.first, - count = chargesInPeriod.size, - totalEnergy = chargesInPeriod.sumOf { it.third.chargeEnergyAdded ?: 0.0 }, - totalCost = chargesInPeriod.sumOf { it.third.cost ?: 0.0 }, - sortKey = key.second - ) - } - .sortedBy { it.sortKey } + .groupBy { it.first to it.second } + .map { (key, list) -> createChargeChartPoint(key.first, key.second, list.map { it.third }) } + .sortedBy { it.sortKey } + } } + // Helper function to centralize chart data creation + private fun createChargeChartPoint(label: String, sortKey: Long, charges: List): ChargeChartData { + return ChargeChartData( + label = label, + count = charges.size, + totalEnergy = charges.sumOf { it.chargeEnergyAdded ?: 0.0 }, + totalCost = charges.sumOf { it.cost ?: 0.0 }, + sortKey = sortKey + ) + } private fun calculateSummary(charges: List): ChargesSummary { if (charges.isEmpty()) return ChargesSummary() diff --git a/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt b/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt index 6ee93775..ec26be7b 100644 --- a/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt @@ -782,14 +782,17 @@ private fun DrivesChartPage( DrivesChartType.TOP_SPEED -> { v -> "${v.toInt()} $speedUnit" } } + // Set number of labels to display + val labelInterval = when { + barData.size <= 7 -> 1 // Show all for Today and last 7 days + barData.size <= 30 -> 3 // Show 1 label every 3 bars for lsat 30 days + else -> ((barData.size + 5) / 6).coerceAtLeast(1) + } val yAxisFormatter: (Double) -> String = when (chartType) { DrivesChartType.TIME -> { v -> formatDurationChart(v.toInt()) } else -> { v -> if (v >= 1000) "%.0fk".format(v / 1000) else "%.0f".format(v) } } - // Show max ~6 labels to avoid crowding - val labelInterval = ((barData.size + 5) / 6).coerceAtLeast(1) - InteractiveBarChart( data = barData, modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/com/matedroid/ui/screens/drives/DrivesViewModel.kt b/app/src/main/java/com/matedroid/ui/screens/drives/DrivesViewModel.kt index 96f2e097..99784ebf 100644 --- a/app/src/main/java/com/matedroid/ui/screens/drives/DrivesViewModel.kt +++ b/app/src/main/java/com/matedroid/ui/screens/drives/DrivesViewModel.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.time.LocalDate +import java.time.LocalDateTime import java.time.YearMonth import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit @@ -122,25 +123,21 @@ class DrivesViewModel @Inject constructor( // Only apply default filter on first initialization if (!isInitialized) { isInitialized = true - applyDateFilterEnum(_uiState.value.dateFilter) + setDateFilter(_uiState.value.dateFilter) } } fun setDateFilter(filter: DriveDateFilter) { - _uiState.update { it.copy(dateFilter = filter) } - applyDateFilterEnum(filter) - } - - private fun applyDateFilterEnum(filter: DriveDateFilter) { - if (filter.days != null) { - val endDate = LocalDate.now() - val startDate = endDate.minusDays(filter.days) - _uiState.update { it.copy(startDate = startDate, endDate = endDate) } - loadDrives(startDate, endDate) - } else { - _uiState.update { it.copy(startDate = null, endDate = null) } - loadDrives(null, null) + val endDate = LocalDate.now() + val startDate = filter.days?.let { days -> + if (days > 0) endDate.minusDays(days - 1) else endDate } + _uiState.update { it.copy( + dateFilter = filter, + startDate = startDate, + endDate = if (filter.days != null) endDate else null + )} + loadDrives(startDate, if (filter.days != null) endDate else null) } fun saveScrollPosition(index: Int, offset: Int) { @@ -252,7 +249,7 @@ class DrivesViewModel @Inject constructor( // Calculate summary and chart data from filtered drives val summary = calculateSummary(drivesForStats) - val chartData = calculateChartData(drivesForStats, granularity) + val chartData = calculateChartData(drivesForStats, granularity, state.startDate) _uiState.update { it.copy( @@ -275,50 +272,77 @@ class DrivesViewModel @Inject constructor( } } - private fun calculateChartData(drives: List, granularity: DriveChartGranularity): List { + private fun calculateChartData(drives: List, granularity: DriveChartGranularity, startDate: LocalDate?): List { if (drives.isEmpty()) return emptyList() val formatter = DateTimeFormatter.ISO_DATE_TIME val weekFields = WeekFields.of(Locale.getDefault()) - return drives - .mapNotNull { drive -> + // Group the drives by day + val drivesByDay = drives.mapNotNull { drive -> + drive.startDate?.let { + try { + val date = LocalDateTime.parse(it, formatter).toLocalDate() + date.toEpochDay() to drive + } catch (e: Exception) { null } + } + }.groupBy({ it.first }, { it.second }) + + return if (granularity == DriveChartGranularity.DAILY) { + // DAILY ranges (today, last 7 and last 30 days + // If not startDate (All Time), get the first trip, or today + val start = startDate ?: (drivesByDay.keys.minOrNull()?.let { LocalDate.ofEpochDay(it) } ?: LocalDate.now()) + val end = LocalDate.now() + val result = mutableListOf() + var current = start + while (!current.isAfter(end)) { + val key = current.toEpochDay() + val drivesInDay = drivesByDay[key] ?: emptyList() + result.add( + createChartPoint( + label = current.format(DateTimeFormatter.ofPattern("d/M")), + sortKey = key, + drives = drivesInDay + ) + ) + current = current.plusDays(1) + } + result + } else { + // WEEKLY and MONTHLY ranges + drives.mapNotNull { drive -> drive.startDate?.let { dateStr -> try { - val date = LocalDate.parse(dateStr, formatter) + val date = LocalDateTime.parse(dateStr, formatter).toLocalDate() val (label, sortKey) = when (granularity) { - DriveChartGranularity.DAILY -> { - val dayLabel = date.format(DateTimeFormatter.ofPattern("d/M")) - dayLabel to date.toEpochDay() - } DriveChartGranularity.WEEKLY -> { - val weekOfYear = date.get(weekFields.weekOfWeekBasedYear()) - val year = date.get(weekFields.weekBasedYear()) - "W$weekOfYear" to (year * 100L + weekOfYear) + val firstDay = date.with(weekFields.dayOfWeek(), 1) + "W${date.get(weekFields.weekOfYear())}" to firstDay.toEpochDay() } - DriveChartGranularity.MONTHLY -> { - val yearMonth = YearMonth.of(date.year, date.month) - yearMonth.format(DateTimeFormatter.ofPattern("MMM yy")) to (date.year * 12L + date.monthValue) + else -> { // MONTHLY + date.format(DateTimeFormatter.ofPattern("MMM yy")) to YearMonth.from(date).atDay(1).toEpochDay() } } Triple(label, sortKey, drive) - } catch (e: Exception) { - null - } + } catch (e: Exception) { null } } } - .groupBy { Pair(it.first, it.second) } - .map { (key, drivesInPeriod) -> - DriveChartData( - label = key.first, - count = drivesInPeriod.size, - totalDistance = drivesInPeriod.sumOf { it.third.distance ?: 0.0 }, - totalDurationMin = drivesInPeriod.sumOf { it.third.durationMin ?: 0 }, - maxSpeed = drivesInPeriod.maxOfOrNull { it.third.speedMax ?: 0 } ?: 0, - sortKey = key.second - ) - } - .sortedBy { it.sortKey } + .groupBy { it.first to it.second } + .map { (key, list) -> createChartPoint(key.first, key.second, list.map { it.third }) } + .sortedBy { it.sortKey } + } + } + + // Helper function to centralize chart data creation + private fun createChartPoint(label: String, sortKey: Long, drives: List): DriveChartData { + return DriveChartData( + label = label, + count = drives.size, + totalDistance = drives.sumOf { it.distance ?: 0.0 }, + totalDurationMin = drives.sumOf { it.durationMin ?: 0 }, + maxSpeed = drives.maxOfOrNull { it.speedMax ?: 0 } ?: 0, + sortKey = sortKey + ) } private fun calculateSummary(drives: List): DrivesSummary {