feat: Add axis labels for charts, add productivity analysis for current week
This commit is contained in:
@@ -40,8 +40,17 @@ interface StatDao {
|
|||||||
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat ORDER BY date DESC LIMIT 7")
|
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat ORDER BY date DESC LIMIT 7")
|
||||||
fun getLastWeekStatsSummary(): Flow<List<StatSummary>>
|
fun getLastWeekStatsSummary(): Flow<List<StatSummary>>
|
||||||
|
|
||||||
@Query("SELECT AVG(focusTimeQ1) AS focusTimeQ1, AVG(focusTimeQ2) AS focusTimeQ2, AVG(focusTimeQ3) AS focusTimeQ3, AVG(focusTimeQ4) AS focusTimeQ4 FROM stat")
|
@Query(
|
||||||
fun getAvgFocusTimes(): Flow<StatFocusTime?>
|
"SELECT " +
|
||||||
|
"AVG(focusTimeQ1) AS focusTimeQ1, " +
|
||||||
|
"AVG(focusTimeQ2) AS focusTimeQ2, " +
|
||||||
|
"AVG(focusTimeQ3) AS focusTimeQ3, " +
|
||||||
|
"AVG(focusTimeQ4) AS focusTimeQ4 " +
|
||||||
|
"FROM (" +
|
||||||
|
"SELECT focusTimeQ1, focusTimeQ2, focusTimeQ3, focusTimeQ4 FROM stat ORDER BY date DESC LIMIT 7" +
|
||||||
|
")"
|
||||||
|
)
|
||||||
|
fun getLastWeekAvgFocusTimes(): Flow<StatFocusTime?>
|
||||||
|
|
||||||
@Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)")
|
@Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)")
|
||||||
suspend fun statExists(date: LocalDate): Boolean
|
suspend fun statExists(date: LocalDate): Boolean
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ interface StatRepository {
|
|||||||
|
|
||||||
fun getLastWeekStatsSummary(): Flow<List<StatSummary>>
|
fun getLastWeekStatsSummary(): Flow<List<StatSummary>>
|
||||||
|
|
||||||
fun getAverageFocusTimes(): Flow<StatFocusTime?>
|
fun getLastWeekAverageFocusTimes(): Flow<StatFocusTime?>
|
||||||
|
|
||||||
suspend fun getLastDate(): LocalDate?
|
suspend fun getLastDate(): LocalDate?
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,8 @@ class AppStatRepository(
|
|||||||
override fun getLastWeekStatsSummary(): Flow<List<StatSummary>> =
|
override fun getLastWeekStatsSummary(): Flow<List<StatSummary>> =
|
||||||
statDao.getLastWeekStatsSummary()
|
statDao.getLastWeekStatsSummary()
|
||||||
|
|
||||||
override fun getAverageFocusTimes(): Flow<StatFocusTime?> = statDao.getAvgFocusTimes()
|
override fun getLastWeekAverageFocusTimes(): Flow<StatFocusTime?> =
|
||||||
|
statDao.getLastWeekAvgFocusTimes()
|
||||||
|
|
||||||
override suspend fun getLastDate(): LocalDate? = statDao.getLastDate()
|
override suspend fun getLastDate(): LocalDate? = statDao.getLastDate()
|
||||||
}
|
}
|
||||||
@@ -25,18 +25,16 @@ import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
|
|||||||
fun ColumnScope.ProductivityGraph(
|
fun ColumnScope.ProductivityGraph(
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
modelProducer: CartesianChartModelProducer,
|
modelProducer: CartesianChartModelProducer,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
label: String = "Productivity analysis"
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(expanded) {
|
AnimatedVisibility(expanded) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Text("Productivity analysis", style = typography.titleMedium)
|
Text(label, style = typography.titleMedium)
|
||||||
Text("Time of day versus focus hours", style = typography.bodySmall)
|
Text("Time of day versus focus hours", style = typography.bodySmall)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
TimeColumnChart(
|
TimeColumnChart(
|
||||||
modelProducer,
|
modelProducer,
|
||||||
yValueFormatter = CartesianValueFormatter { _, value, _ ->
|
|
||||||
millisecondsToHoursMinutes(value.toLong())
|
|
||||||
},
|
|
||||||
xValueFormatter = CartesianValueFormatter { _, value, _ ->
|
xValueFormatter = CartesianValueFormatter { _, value, _ ->
|
||||||
when (value) {
|
when (value) {
|
||||||
0.0 -> "0 - 6"
|
0.0 -> "0 - 6"
|
||||||
@@ -45,7 +43,11 @@ fun ColumnScope.ProductivityGraph(
|
|||||||
3.0 -> "18 - 24"
|
3.0 -> "18 - 24"
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
yValueFormatter = CartesianValueFormatter { _, value, _ ->
|
||||||
|
millisecondsToHoursMinutes(value.toLong())
|
||||||
|
},
|
||||||
|
animationSpec = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
|
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
||||||
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
|
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
|
||||||
|
import com.patrykandpatrick.vico.core.common.data.ExtraStore
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.nsh07.pomodoro.R
|
import org.nsh07.pomodoro.R
|
||||||
import org.nsh07.pomodoro.data.Stat
|
import org.nsh07.pomodoro.data.Stat
|
||||||
@@ -65,8 +67,9 @@ fun StatsScreenRoot(
|
|||||||
) {
|
) {
|
||||||
val todayStat by viewModel.todayStat.collectAsState(null)
|
val todayStat by viewModel.todayStat.collectAsState(null)
|
||||||
StatsScreen(
|
StatsScreen(
|
||||||
lastWeekSummaryModelProducer = viewModel.lastWeekSummaryChartModelProducer,
|
lastWeekSummaryChartData = viewModel.lastWeekSummaryChartData,
|
||||||
todayStatModelProducer = viewModel.todayStatModelProducer,
|
lastWeekSummaryAnalysisModelProducer = viewModel.lastWeekSummaryAnalysisModelProducer,
|
||||||
|
todayStatAnalysisModelProducer = viewModel.todayStatAnalysisModelProducer,
|
||||||
todayStat = todayStat,
|
todayStat = todayStat,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
@@ -75,14 +78,16 @@ fun StatsScreenRoot(
|
|||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun StatsScreen(
|
fun StatsScreen(
|
||||||
lastWeekSummaryModelProducer: CartesianChartModelProducer,
|
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
todayStatModelProducer: CartesianChartModelProducer,
|
lastWeekSummaryAnalysisModelProducer: CartesianChartModelProducer,
|
||||||
|
todayStatAnalysisModelProducer: CartesianChartModelProducer,
|
||||||
todayStat: Stat?,
|
todayStat: Stat?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||||
|
|
||||||
var todayStatExpanded by rememberSaveable { mutableStateOf(false) }
|
var todayStatExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var lastWeekStatExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -181,6 +186,7 @@ fun StatsScreen(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
|
Spacer(Modifier.height(2.dp))
|
||||||
FilledTonalIconToggleButton(
|
FilledTonalIconToggleButton(
|
||||||
checked = todayStatExpanded,
|
checked = todayStatExpanded,
|
||||||
onCheckedChange = { todayStatExpanded = it },
|
onCheckedChange = { todayStatExpanded = it },
|
||||||
@@ -198,7 +204,7 @@ fun StatsScreen(
|
|||||||
}
|
}
|
||||||
ProductivityGraph(
|
ProductivityGraph(
|
||||||
todayStatExpanded,
|
todayStatExpanded,
|
||||||
todayStatModelProducer,
|
todayStatAnalysisModelProducer,
|
||||||
Modifier.padding(horizontal = 32.dp)
|
Modifier.padding(horizontal = 32.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -215,10 +221,47 @@ fun StatsScreen(
|
|||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
TimeColumnChart(
|
TimeColumnChart(
|
||||||
lastWeekSummaryModelProducer,
|
lastWeekSummaryChartData.first,
|
||||||
modifier = Modifier.padding(start = 16.dp)
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
|
xValueFormatter = CartesianValueFormatter { context, x, _ ->
|
||||||
|
context.model.extraStore[lastWeekSummaryChartData.second][x.toInt()]
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
val iconRotation by animateFloatAsState(
|
||||||
|
if (lastWeekStatExpanded) 180f else 0f,
|
||||||
|
animationSpec = motionScheme.defaultSpatialSpec()
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(2.dp))
|
||||||
|
FilledTonalIconToggleButton(
|
||||||
|
checked = lastWeekStatExpanded,
|
||||||
|
onCheckedChange = { lastWeekStatExpanded = it },
|
||||||
|
shapes = IconButtonDefaults.toggleableShapes(),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.width(52.dp)
|
||||||
|
.align(Alignment.End)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.arrow_down),
|
||||||
|
"More info",
|
||||||
|
modifier = Modifier.rotate(iconRotation)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ProductivityGraph(
|
||||||
|
lastWeekStatExpanded,
|
||||||
|
lastWeekSummaryAnalysisModelProducer,
|
||||||
|
label = "Weekly productivity analysis",
|
||||||
|
modifier = Modifier.padding(horizontal = 32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,6 +283,7 @@ fun StatsScreenPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StatsScreen(
|
StatsScreen(
|
||||||
|
Pair(modelProducer, ExtraStore.Key()),
|
||||||
modelProducer,
|
modelProducer,
|
||||||
modelProducer,
|
modelProducer,
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ package org.nsh07.pomodoro.ui.statsScreen
|
|||||||
|
|
||||||
import android.graphics.Path
|
import android.graphics.Path
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -45,7 +47,8 @@ internal fun TimeColumnChart(
|
|||||||
xValueFormatter: CartesianValueFormatter = CartesianValueFormatter.Default,
|
xValueFormatter: CartesianValueFormatter = CartesianValueFormatter.Default,
|
||||||
yValueFormatter: CartesianValueFormatter = CartesianValueFormatter { measuringContext, value, _ ->
|
yValueFormatter: CartesianValueFormatter = CartesianValueFormatter { measuringContext, value, _ ->
|
||||||
millisecondsToHours(value.toLong())
|
millisecondsToHours(value.toLong())
|
||||||
}
|
},
|
||||||
|
animationSpec: AnimationSpec<Float>? = tween(500)
|
||||||
) {
|
) {
|
||||||
val radius = with(LocalDensity.current) {
|
val radius = with(LocalDensity.current) {
|
||||||
(thickness / 2).toPx()
|
(thickness / 2).toPx()
|
||||||
@@ -107,6 +110,7 @@ internal fun TimeColumnChart(
|
|||||||
initialZoom = Zoom.fixed(),
|
initialZoom = Zoom.fixed(),
|
||||||
minZoom = Zoom.min(Zoom.Content, Zoom.fixed())
|
minZoom = Zoom.min(Zoom.Content, Zoom.fixed())
|
||||||
),
|
),
|
||||||
|
animationSpec = animationSpec,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,38 +15,61 @@ import androidx.lifecycle.viewmodel.initializer
|
|||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
|
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
|
||||||
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
|
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
|
||||||
|
import com.patrykandpatrick.vico.core.common.data.ExtraStore
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.nsh07.pomodoro.TomatoApplication
|
import org.nsh07.pomodoro.TomatoApplication
|
||||||
import org.nsh07.pomodoro.data.StatRepository
|
import org.nsh07.pomodoro.data.StatRepository
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
class StatsViewModel(
|
class StatsViewModel(
|
||||||
statRepository: StatRepository
|
statRepository: StatRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val todayStat = statRepository.getTodayStat().distinctUntilChanged()
|
private val dayFormatter = DateTimeFormatter.ofPattern("E")
|
||||||
private val allStatsSummary = statRepository.getLastWeekStatsSummary()
|
|
||||||
private val averageFocusTimes = statRepository.getAverageFocusTimes()
|
|
||||||
|
|
||||||
val lastWeekSummaryChartModelProducer = CartesianChartModelProducer()
|
val todayStat = statRepository.getTodayStat().distinctUntilChanged()
|
||||||
val todayStatModelProducer = CartesianChartModelProducer()
|
private val lastWeekStatsSummary = statRepository.getLastWeekStatsSummary()
|
||||||
|
private val lastWeekAverageFocusTimes = statRepository.getLastWeekAverageFocusTimes()
|
||||||
|
|
||||||
|
val lastWeekSummaryChartData =
|
||||||
|
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
|
||||||
|
val lastWeekSummaryAnalysisModelProducer = CartesianChartModelProducer()
|
||||||
|
val todayStatAnalysisModelProducer = CartesianChartModelProducer()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
allStatsSummary
|
lastWeekStatsSummary
|
||||||
.collect { list ->
|
.collect { list ->
|
||||||
lastWeekSummaryChartModelProducer.runTransaction {
|
// reversing is required because we need ascending order while the DB returns descending order
|
||||||
columnSeries {
|
val reversed = list.reversed()
|
||||||
// reversing is required because we need ascending order while the DB returns descending order
|
val keys = reversed.map { it.date.format(dayFormatter) }
|
||||||
series(list.reversed().map { it.focusTime })
|
val values = reversed.map { it.focusTime }
|
||||||
}
|
lastWeekSummaryChartData.first.runTransaction {
|
||||||
|
columnSeries { series(values) }
|
||||||
|
extras { it[lastWeekSummaryChartData.second] = keys }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
todayStat
|
todayStat
|
||||||
.collect {
|
.collect {
|
||||||
todayStatModelProducer.runTransaction {
|
todayStatAnalysisModelProducer.runTransaction {
|
||||||
|
columnSeries {
|
||||||
|
series(
|
||||||
|
it?.focusTimeQ1 ?: 0,
|
||||||
|
it?.focusTimeQ2 ?: 0,
|
||||||
|
it?.focusTimeQ3 ?: 0,
|
||||||
|
it?.focusTimeQ4 ?: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
lastWeekAverageFocusTimes
|
||||||
|
.collect {
|
||||||
|
lastWeekSummaryAnalysisModelProducer.runTransaction {
|
||||||
columnSeries {
|
columnSeries {
|
||||||
series(
|
series(
|
||||||
it?.focusTimeQ1 ?: 0,
|
it?.focusTimeQ1 ?: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user