Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -826,8 +826,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 lsat 30 days
else -> ((barData.size + 5) / 6).coerceAtLeast(1)
}

InteractiveBarChart(
data = barData,
Expand Down
101 changes: 65 additions & 36 deletions app/src/main/java/com/matedroid/ui/screens/charges/ChargesViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) }
Expand Down Expand Up @@ -251,7 +253,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(
Expand All @@ -274,51 +276,78 @@ class ChargesViewModel @Inject constructor(
}
}

private fun calculateChartData(charges: List<ChargeData>, granularity: ChartGranularity): List<ChargeChartData> {
private fun calculateChartData(charges: List<ChargeData>, granularity: ChartGranularity, startDate: LocalDate?): List<ChargeChartData> {
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<ChargeChartData>()
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<ChargeData>): 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<ChargeData>): ChargesSummary {
if (charges.isEmpty()) return ChargesSummary()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
112 changes: 68 additions & 44 deletions app/src/main/java/com/matedroid/ui/screens/drives/DrivesViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -251,7 +248,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(
Expand All @@ -274,50 +271,77 @@ class DrivesViewModel @Inject constructor(
}
}

private fun calculateChartData(drives: List<DriveData>, granularity: DriveChartGranularity): List<DriveChartData> {
private fun calculateChartData(drives: List<DriveData>, granularity: DriveChartGranularity, startDate: LocalDate?): List<DriveChartData> {
if (drives.isEmpty()) return emptyList()

val formatter = DateTimeFormatter.ISO_DATE_TIME
val weekFields = WeekFields.of(Locale.getDefault())

return drives
.mapNotNull { drive ->
// Group the charges 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<DriveChartData>()
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<DriveData>): 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<DriveData>): DrivesSummary {
Expand Down