From 09c7ca45594ee254f9dec4920317f3ce9132bc7e Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 16 Oct 2025 00:04:33 +0530 Subject: [PATCH] feat(stats): implement showing last year stats in a line chart --- .../pomodoro/ui/statsScreen/StatsScreen.kt | 96 ++++++++++++++----- .../pomodoro/ui/statsScreen/TimeLineChart.kt | 2 +- .../statsScreen/viewModel/StatsViewModel.kt | 41 +++++++- app/src/main/res/values/strings.xml | 1 + 4 files changed, 113 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt index 3af64e8..1f7c8c3 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt @@ -78,34 +78,18 @@ fun StatsScreenRoot( val lastMonthSummaryChartData by viewModel.lastMonthSummaryChartData.collectAsStateWithLifecycle() val lastMonthAnalysisValues by viewModel.lastMonthAverageFocusTimes.collectAsStateWithLifecycle() - val lastWeekSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() } - val lastMonthSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() } - - LaunchedEffect(lastWeekAnalysisValues) { - lastWeekSummaryAnalysisModelProducer.runTransaction { - columnSeries { - series(lastWeekAnalysisValues) - } - } - } - - LaunchedEffect(lastMonthAnalysisValues) { - lastMonthSummaryAnalysisModelProducer.runTransaction { - columnSeries { - series(lastMonthAnalysisValues) - } - } - } + val lastYearSummaryChartData by viewModel.lastYearSummaryChartData.collectAsStateWithLifecycle() + val lastYearAnalysisValues by viewModel.lastYearAverageFocusTimes.collectAsStateWithLifecycle() StatsScreen( contentPadding = contentPadding, lastWeekSummaryChartData = lastWeekSummaryChartData, - lastWeekSummaryAnalysisModelProducer = lastWeekSummaryAnalysisModelProducer, lastMonthSummaryChartData = lastMonthSummaryChartData, - lastMonthSummaryAnalysisModelProducer = lastMonthSummaryAnalysisModelProducer, + lastYearSummaryChartData = lastYearSummaryChartData, todayStat = todayStat, lastWeekAverageFocusTimes = lastWeekAnalysisValues, lastMonthAverageFocusTimes = lastMonthAnalysisValues, + lastYearAverageFocusTimes = lastYearAnalysisValues, modifier = modifier ) } @@ -115,12 +99,12 @@ fun StatsScreenRoot( fun StatsScreen( contentPadding: PaddingValues, lastWeekSummaryChartData: Pair>>, - lastWeekSummaryAnalysisModelProducer: CartesianChartModelProducer, lastMonthSummaryChartData: Pair>>, - lastMonthSummaryAnalysisModelProducer: CartesianChartModelProducer, + lastYearSummaryChartData: Pair>>, todayStat: Stat?, lastWeekAverageFocusTimes: List, lastMonthAverageFocusTimes: List, + lastYearAverageFocusTimes: List, modifier: Modifier = Modifier ) { val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() @@ -128,6 +112,25 @@ fun StatsScreen( var lastWeekStatExpanded by rememberSaveable { mutableStateOf(false) } var lastMonthStatExpanded by rememberSaveable { mutableStateOf(false) } + val lastWeekSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() } + val lastMonthSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() } + + LaunchedEffect(lastWeekAverageFocusTimes) { + lastWeekSummaryAnalysisModelProducer.runTransaction { + columnSeries { + series(lastWeekAverageFocusTimes) + } + } + } + + LaunchedEffect(lastMonthAverageFocusTimes) { + lastMonthSummaryAnalysisModelProducer.runTransaction { + columnSeries { + series(lastMonthAverageFocusTimes) + } + } + } + Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) @@ -371,8 +374,51 @@ fun StatsScreen( modifier = Modifier.padding(horizontal = 32.dp) ) } - Spacer(Modifier.height(16.dp)) } + item { Spacer(Modifier) } + item { + Text( + stringResource(R.string.last_year), + style = typography.headlineSmall, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } + item { + Row( + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Text( + millisecondsToHoursMinutes( + remember(lastYearAverageFocusTimes) { + lastYearAverageFocusTimes.sum().toLong() + } + ), + style = typography.displaySmall, + fontFamily = openRundeClock + ) + Text( + text = stringResource(R.string.focus_per_day_avg), + style = typography.titleSmall, + modifier = Modifier.padding(bottom = 6.3.dp) + ) + } + } + item { + TimeLineChart( + lastYearSummaryChartData.first, + modifier = Modifier.padding(start = 16.dp), + xValueFormatter = CartesianValueFormatter { context, x, _ -> + context.model.extraStore[lastYearSummaryChartData.second][x.toInt()] + } + ) + } + item { Spacer(Modifier.height(16.dp)) } } } } @@ -398,11 +444,11 @@ fun StatsScreenPreview() { StatsScreen( PaddingValues(), Pair(modelProducer, keys), - modelProducer, Pair(modelProducer, keys), - modelProducer, + Pair(modelProducer, keys), null, listOf(0, 0, 0, 0), + listOf(0, 0, 0, 0), listOf(0, 0, 0, 0) ) } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeLineChart.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeLineChart.kt index 00855dd..73485f9 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeLineChart.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeLineChart.kt @@ -49,7 +49,7 @@ import org.nsh07.pomodoro.utils.millisecondsToMinutes @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -internal fun TimeLineChart( +fun TimeLineChart( modelProducer: CartesianChartModelProducer, modifier: Modifier = Modifier, thickness: Float = 2f, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt index 9f0e553..2b907d5 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.columnSeries +import com.patrykandpatrick.vico.core.cartesian.data.lineSeries import com.patrykandpatrick.vico.core.common.data.ExtraStore import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -23,6 +24,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.data.StatRepository +import java.time.format.DateTimeFormatter import java.time.format.TextStyle import java.util.Locale @@ -34,9 +36,12 @@ class StatsViewModel( private val lastWeekSummary = Pair(CartesianChartModelProducer(), ExtraStore.Key>()) - private val lastMonthSummary = Pair(CartesianChartModelProducer(), ExtraStore.Key>()) + private val lastYearSummary = + Pair(CartesianChartModelProducer(), ExtraStore.Key>()) + + private val yearDayFormatter = DateTimeFormatter.ofPattern("d MMM") val lastWeekSummaryChartData: StateFlow>>> = statRepository.getLastNDaysStatsSummary(7) @@ -112,6 +117,40 @@ class StatsViewModel( initialValue = listOf(0, 0, 0, 0) ) + val lastYearSummaryChartData: StateFlow>>> = + statRepository.getLastNDaysStatsSummary(365) + .map { list -> + val reversed = list.reversed() + val keys = reversed.map { it.date.format(yearDayFormatter) } + val values = reversed.map { it.focusTime } + lastYearSummary.first.runTransaction { + lineSeries { series(values) } + extras { it[lastYearSummary.second] = keys } + } + lastYearSummary + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = lastYearSummary + ) + + val lastYearAverageFocusTimes: StateFlow> = + statRepository.getLastNDaysAverageFocusTimes(365) + .map { + listOf( + it?.focusTimeQ1?.toInt() ?: 0, + it?.focusTimeQ2?.toInt() ?: 0, + it?.focusTimeQ3?.toInt() ?: 0, + it?.focusTimeQ4?.toInt() ?: 0 + ) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = listOf(0, 0, 0, 0) + ) + companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d55c21..067cff5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,4 +56,5 @@ Up next Timer Timer progress + Last year \ No newline at end of file