Merge pull request #72 from nsh07/year-stats
feat: implement a time line chart composable
This commit is contained in:
@@ -35,6 +35,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -56,10 +57,8 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
|
|||||||
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
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 com.patrykandpatrick.vico.core.common.data.ExtraStore
|
||||||
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
|
||||||
import org.nsh07.pomodoro.data.StatFocusTime
|
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
|
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
@@ -72,20 +71,25 @@ fun StatsScreenRoot(
|
|||||||
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
|
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
|
||||||
) {
|
) {
|
||||||
val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null)
|
val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null)
|
||||||
val lastWeekAverageFocusTimes by viewModel
|
|
||||||
.lastWeekAverageFocusTimes.collectAsStateWithLifecycle(null)
|
val lastWeekSummaryChartData by viewModel.lastWeekSummaryChartData.collectAsStateWithLifecycle()
|
||||||
val lastMonthAverageFocusTimes by viewModel
|
val lastWeekAnalysisValues by viewModel.lastWeekAverageFocusTimes.collectAsStateWithLifecycle()
|
||||||
.lastMonthAverageFocusTimes.collectAsStateWithLifecycle(null)
|
|
||||||
|
val lastMonthSummaryChartData by viewModel.lastMonthSummaryChartData.collectAsStateWithLifecycle()
|
||||||
|
val lastMonthAnalysisValues by viewModel.lastMonthAverageFocusTimes.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val lastYearSummaryChartData by viewModel.lastYearSummaryChartData.collectAsStateWithLifecycle()
|
||||||
|
val lastYearAnalysisValues by viewModel.lastYearAverageFocusTimes.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
StatsScreen(
|
StatsScreen(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
lastWeekSummaryChartData = remember { viewModel.lastWeekSummaryChartData },
|
lastWeekSummaryChartData = lastWeekSummaryChartData,
|
||||||
lastWeekSummaryAnalysisModelProducer = remember { viewModel.lastWeekSummaryAnalysisModelProducer },
|
lastMonthSummaryChartData = lastMonthSummaryChartData,
|
||||||
lastMonthSummaryChartData = remember { viewModel.lastMonthSummaryChartData },
|
lastYearSummaryChartData = lastYearSummaryChartData,
|
||||||
lastMonthSummaryAnalysisModelProducer = remember { viewModel.lastMonthSummaryAnalysisModelProducer },
|
|
||||||
todayStat = todayStat,
|
todayStat = todayStat,
|
||||||
lastWeekAverageFocusTimes = lastWeekAverageFocusTimes,
|
lastWeekAverageFocusTimes = lastWeekAnalysisValues,
|
||||||
lastMonthAverageFocusTimes = lastMonthAverageFocusTimes,
|
lastMonthAverageFocusTimes = lastMonthAnalysisValues,
|
||||||
|
lastYearAverageFocusTimes = lastYearAnalysisValues,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -95,12 +99,12 @@ fun StatsScreenRoot(
|
|||||||
fun StatsScreen(
|
fun StatsScreen(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
lastWeekSummaryAnalysisModelProducer: CartesianChartModelProducer,
|
|
||||||
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
lastMonthSummaryAnalysisModelProducer: CartesianChartModelProducer,
|
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
todayStat: Stat?,
|
todayStat: Stat?,
|
||||||
lastWeekAverageFocusTimes: StatFocusTime?,
|
lastWeekAverageFocusTimes: List<Int>,
|
||||||
lastMonthAverageFocusTimes: StatFocusTime?,
|
lastMonthAverageFocusTimes: List<Int>,
|
||||||
|
lastYearAverageFocusTimes: List<Int>,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||||
@@ -108,6 +112,25 @@ fun StatsScreen(
|
|||||||
var lastWeekStatExpanded by rememberSaveable { mutableStateOf(false) }
|
var lastWeekStatExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
var lastMonthStatExpanded 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(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
@@ -218,7 +241,11 @@ fun StatsScreen(
|
|||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
millisecondsToHoursMinutes(lastWeekAverageFocusTimes?.total() ?: 0),
|
millisecondsToHoursMinutes(
|
||||||
|
remember(lastWeekAverageFocusTimes) {
|
||||||
|
lastWeekAverageFocusTimes.sum().toLong()
|
||||||
|
}
|
||||||
|
),
|
||||||
style = typography.displaySmall,
|
style = typography.displaySmall,
|
||||||
fontFamily = openRundeClock
|
fontFamily = openRundeClock
|
||||||
)
|
)
|
||||||
@@ -290,7 +317,11 @@ fun StatsScreen(
|
|||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
millisecondsToHoursMinutes(lastMonthAverageFocusTimes?.total() ?: 0),
|
millisecondsToHoursMinutes(
|
||||||
|
remember(lastMonthAverageFocusTimes) {
|
||||||
|
lastMonthAverageFocusTimes.sum().toLong()
|
||||||
|
}
|
||||||
|
),
|
||||||
style = typography.displaySmall,
|
style = typography.displaySmall,
|
||||||
fontFamily = openRundeClock
|
fontFamily = openRundeClock
|
||||||
)
|
)
|
||||||
@@ -343,8 +374,51 @@ fun StatsScreen(
|
|||||||
modifier = Modifier.padding(horizontal = 32.dp)
|
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)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,23 +430,25 @@ fun StatsScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
fun StatsScreenPreview() {
|
fun StatsScreenPreview() {
|
||||||
val modelProducer = remember { CartesianChartModelProducer() }
|
val modelProducer = remember { CartesianChartModelProducer() }
|
||||||
|
val keys = remember { ExtraStore.Key<List<String>>() }
|
||||||
|
|
||||||
runBlocking {
|
LaunchedEffect(Unit) {
|
||||||
modelProducer.runTransaction {
|
modelProducer.runTransaction {
|
||||||
columnSeries {
|
columnSeries {
|
||||||
series(5, 6, 5, 2, 11, 8, 5, 2, 15, 11, 8, 13, 12, 10, 2, 7)
|
series(5, 6, 5, 2, 11, 8, 5, 2, 15, 11, 8, 13, 12, 10, 2, 7)
|
||||||
}
|
}
|
||||||
|
extras { it[keys] = listOf("M", "T", "W", "T", "F", "S", "S") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatsScreen(
|
StatsScreen(
|
||||||
PaddingValues(),
|
PaddingValues(),
|
||||||
Pair(modelProducer, ExtraStore.Key()),
|
Pair(modelProducer, keys),
|
||||||
modelProducer,
|
Pair(modelProducer, keys),
|
||||||
Pair(modelProducer, ExtraStore.Key()),
|
Pair(modelProducer, keys),
|
||||||
modelProducer,
|
|
||||||
null,
|
null,
|
||||||
null,
|
listOf(0, 0, 0, 0),
|
||||||
null
|
listOf(0, 0, 0, 0),
|
||||||
|
listOf(0, 0, 0, 0)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,12 @@ package org.nsh07.pomodoro.ui.statsScreen
|
|||||||
import androidx.compose.animation.core.AnimationSpec
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.MaterialTheme.motionScheme
|
import androidx.compose.material3.MaterialTheme.motionScheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
||||||
@@ -25,20 +29,23 @@ import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent
|
|||||||
import com.patrykandpatrick.vico.compose.common.fill
|
import com.patrykandpatrick.vico.compose.common.fill
|
||||||
import com.patrykandpatrick.vico.compose.common.vicoTheme
|
import com.patrykandpatrick.vico.compose.common.vicoTheme
|
||||||
import com.patrykandpatrick.vico.compose.m3.common.rememberM3VicoTheme
|
import com.patrykandpatrick.vico.compose.m3.common.rememberM3VicoTheme
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.FadingEdges
|
||||||
import com.patrykandpatrick.vico.core.cartesian.Zoom
|
import com.patrykandpatrick.vico.core.cartesian.Zoom
|
||||||
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
|
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
|
||||||
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
|
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
|
||||||
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.CartesianValueFormatter
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
|
||||||
import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer
|
import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer
|
||||||
import com.patrykandpatrick.vico.core.common.Fill
|
import com.patrykandpatrick.vico.core.common.Fill
|
||||||
import com.patrykandpatrick.vico.core.common.shape.CorneredShape
|
import com.patrykandpatrick.vico.core.common.shape.CorneredShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||||
import org.nsh07.pomodoro.utils.millisecondsToHours
|
import org.nsh07.pomodoro.utils.millisecondsToHours
|
||||||
import org.nsh07.pomodoro.utils.millisecondsToMinutes
|
import org.nsh07.pomodoro.utils.millisecondsToMinutes
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimeColumnChart(
|
fun TimeColumnChart(
|
||||||
modelProducer: CartesianChartModelProducer,
|
modelProducer: CartesianChartModelProducer,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
thickness: Dp = 40.dp,
|
thickness: Dp = 40.dp,
|
||||||
@@ -80,7 +87,8 @@ internal fun TimeColumnChart(
|
|||||||
tick = rememberLineComponent(Fill.Transparent),
|
tick = rememberLineComponent(Fill.Transparent),
|
||||||
guideline = rememberLineComponent(Fill.Transparent),
|
guideline = rememberLineComponent(Fill.Transparent),
|
||||||
valueFormatter = xValueFormatter
|
valueFormatter = xValueFormatter
|
||||||
)
|
),
|
||||||
|
fadingEdges = FadingEdges()
|
||||||
),
|
),
|
||||||
modelProducer = modelProducer,
|
modelProducer = modelProducer,
|
||||||
zoomState = rememberVicoZoomState(
|
zoomState = rememberVicoZoomState(
|
||||||
@@ -92,4 +100,26 @@ internal fun TimeColumnChart(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun TimeColumnChartPreview() {
|
||||||
|
val modelProducer = remember { CartesianChartModelProducer() }
|
||||||
|
val values = mutableListOf<Int>()
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
repeat(30) {
|
||||||
|
values.add((0..120).random() * 60 * 1000)
|
||||||
|
}
|
||||||
|
modelProducer.runTransaction {
|
||||||
|
columnSeries {
|
||||||
|
series(values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TomatoTheme {
|
||||||
|
Surface {
|
||||||
|
TimeColumnChart(thickness = 8.dp, modelProducer = modelProducer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.statsScreen
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.MaterialTheme.motionScheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
|
||||||
|
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottom
|
||||||
|
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStart
|
||||||
|
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLine
|
||||||
|
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
|
||||||
|
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
|
||||||
|
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState
|
||||||
|
import com.patrykandpatrick.vico.compose.common.ProvideVicoTheme
|
||||||
|
import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent
|
||||||
|
import com.patrykandpatrick.vico.compose.common.fill
|
||||||
|
import com.patrykandpatrick.vico.compose.common.vicoTheme
|
||||||
|
import com.patrykandpatrick.vico.compose.m3.common.rememberM3VicoTheme
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.FadingEdges
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.Zoom
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.data.lineSeries
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer.LineFill.Companion.single
|
||||||
|
import com.patrykandpatrick.vico.core.common.Fill
|
||||||
|
import com.patrykandpatrick.vico.core.common.shader.ShaderProvider
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||||
|
import org.nsh07.pomodoro.utils.millisecondsToHours
|
||||||
|
import org.nsh07.pomodoro.utils.millisecondsToMinutes
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TimeLineChart(
|
||||||
|
modelProducer: CartesianChartModelProducer,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
thickness: Float = 2f,
|
||||||
|
pointSpacing: Dp = 12.dp,
|
||||||
|
xValueFormatter: CartesianValueFormatter = CartesianValueFormatter.Default,
|
||||||
|
yValueFormatter: CartesianValueFormatter = CartesianValueFormatter { _, value, _ ->
|
||||||
|
if (value >= 60 * 60 * 1000) {
|
||||||
|
millisecondsToHours(value.toLong())
|
||||||
|
} else {
|
||||||
|
millisecondsToMinutes(value.toLong())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animationSpec: AnimationSpec<Float>? = motionScheme.slowEffectsSpec()
|
||||||
|
) {
|
||||||
|
ProvideVicoTheme(rememberM3VicoTheme()) {
|
||||||
|
CartesianChartHost(
|
||||||
|
chart =
|
||||||
|
rememberCartesianChart(
|
||||||
|
rememberLineCartesianLayer(
|
||||||
|
LineCartesianLayer.LineProvider.series(
|
||||||
|
vicoTheme.lineCartesianLayerColors.map { color ->
|
||||||
|
LineCartesianLayer.rememberLine(
|
||||||
|
fill = single(fill(color)),
|
||||||
|
stroke = LineCartesianLayer.LineStroke.Continuous(
|
||||||
|
thicknessDp = thickness,
|
||||||
|
),
|
||||||
|
areaFill = LineCartesianLayer.AreaFill.single(
|
||||||
|
fill(
|
||||||
|
ShaderProvider.verticalGradient(
|
||||||
|
color.toArgb(),
|
||||||
|
Color.Transparent.toArgb()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
pointConnector = LineCartesianLayer.PointConnector.cubic(0.5f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
pointSpacing = pointSpacing
|
||||||
|
),
|
||||||
|
startAxis = VerticalAxis.rememberStart(
|
||||||
|
line = rememberLineComponent(Fill.Transparent),
|
||||||
|
tick = rememberLineComponent(Fill.Transparent),
|
||||||
|
guideline = rememberLineComponent(Fill.Transparent),
|
||||||
|
valueFormatter = yValueFormatter
|
||||||
|
),
|
||||||
|
bottomAxis = HorizontalAxis.rememberBottom(
|
||||||
|
rememberLineComponent(Fill.Transparent),
|
||||||
|
tick = rememberLineComponent(Fill.Transparent),
|
||||||
|
guideline = rememberLineComponent(Fill.Transparent),
|
||||||
|
valueFormatter = xValueFormatter
|
||||||
|
),
|
||||||
|
fadingEdges = FadingEdges()
|
||||||
|
),
|
||||||
|
modelProducer = modelProducer,
|
||||||
|
zoomState = rememberVicoZoomState(
|
||||||
|
zoomEnabled = true,
|
||||||
|
initialZoom = Zoom.fixed(),
|
||||||
|
minZoom = Zoom.min(Zoom.Content, Zoom.fixed())
|
||||||
|
),
|
||||||
|
animationSpec = animationSpec,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun TimeLineChartPreview() {
|
||||||
|
val modelProducer = remember { CartesianChartModelProducer() }
|
||||||
|
val values = mutableListOf<Int>()
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
repeat(365) {
|
||||||
|
values.add((0..120).random() * 60 * 1000)
|
||||||
|
}
|
||||||
|
modelProducer.runTransaction {
|
||||||
|
lineSeries {
|
||||||
|
series(values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TomatoTheme {
|
||||||
|
Surface {
|
||||||
|
TimeLineChart(modelProducer = modelProducer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,12 +15,16 @@ 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.cartesian.data.lineSeries
|
||||||
import com.patrykandpatrick.vico.core.common.data.ExtraStore
|
import com.patrykandpatrick.vico.core.common.data.ExtraStore
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
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
|
||||||
import java.time.format.TextStyle
|
import java.time.format.TextStyle
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@@ -29,83 +33,123 @@ class StatsViewModel(
|
|||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val todayStat = statRepository.getTodayStat().distinctUntilChanged()
|
val todayStat = statRepository.getTodayStat().distinctUntilChanged()
|
||||||
private val lastWeekStatsSummary = statRepository.getLastNDaysStatsSummary(7)
|
|
||||||
val lastWeekAverageFocusTimes =
|
|
||||||
statRepository.getLastNDaysAverageFocusTimes(7).distinctUntilChanged()
|
|
||||||
private val lastMonthStatsSummary = statRepository.getLastNDaysStatsSummary(30)
|
|
||||||
val lastMonthAverageFocusTimes =
|
|
||||||
statRepository.getLastNDaysAverageFocusTimes(30).distinctUntilChanged()
|
|
||||||
|
|
||||||
val lastWeekSummaryChartData =
|
private val lastWeekSummary =
|
||||||
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
|
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
|
||||||
val lastWeekSummaryAnalysisModelProducer = CartesianChartModelProducer()
|
private val lastMonthSummary =
|
||||||
|
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
|
||||||
val lastMonthSummaryChartData =
|
private val lastYearSummary =
|
||||||
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
|
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
|
||||||
val lastMonthSummaryAnalysisModelProducer = CartesianChartModelProducer()
|
|
||||||
|
|
||||||
init {
|
private val yearDayFormatter = DateTimeFormatter.ofPattern("d MMM")
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
lastWeekStatsSummary
|
val lastWeekSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
|
||||||
.collect { list ->
|
statRepository.getLastNDaysStatsSummary(7)
|
||||||
// reversing is required because we need ascending order while the DB returns descending order
|
.map { list ->
|
||||||
val reversed = list.reversed()
|
// reversing is required because we need ascending order while the DB returns descending order
|
||||||
val keys = reversed.map {
|
val reversed = list.reversed()
|
||||||
it.date.dayOfWeek.getDisplayName(
|
val keys = reversed.map {
|
||||||
TextStyle.NARROW,
|
it.date.dayOfWeek.getDisplayName(
|
||||||
Locale.getDefault()
|
TextStyle.NARROW,
|
||||||
)
|
Locale.getDefault()
|
||||||
}
|
)
|
||||||
val values = reversed.map { it.focusTime }
|
|
||||||
lastWeekSummaryChartData.first.runTransaction {
|
|
||||||
columnSeries { series(values) }
|
|
||||||
extras { it[lastWeekSummaryChartData.second] = keys }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
val values = reversed.map { it.focusTime }
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
lastWeekSummary.first.runTransaction {
|
||||||
lastWeekAverageFocusTimes
|
columnSeries { series(values) }
|
||||||
.collect {
|
extras { it[lastWeekSummary.second] = keys }
|
||||||
lastWeekSummaryAnalysisModelProducer.runTransaction {
|
|
||||||
columnSeries {
|
|
||||||
series(
|
|
||||||
it?.focusTimeQ1 ?: 0,
|
|
||||||
it?.focusTimeQ2 ?: 0,
|
|
||||||
it?.focusTimeQ3 ?: 0,
|
|
||||||
it?.focusTimeQ4 ?: 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
lastWeekSummary
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
}
|
||||||
lastMonthStatsSummary
|
.stateIn(
|
||||||
.collect { list ->
|
scope = viewModelScope,
|
||||||
val reversed = list.reversed()
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
val keys = reversed.map { it.date.dayOfMonth.toString() }
|
initialValue = lastWeekSummary
|
||||||
val values = reversed.map { it.focusTime }
|
)
|
||||||
lastMonthSummaryChartData.first.runTransaction {
|
|
||||||
columnSeries { series(values) }
|
val lastWeekAverageFocusTimes: StateFlow<List<Int>> =
|
||||||
extras { it[lastMonthSummaryChartData.second] = keys }
|
statRepository.getLastNDaysAverageFocusTimes(7)
|
||||||
}
|
.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)
|
||||||
|
)
|
||||||
|
|
||||||
|
val lastMonthSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
|
||||||
|
statRepository.getLastNDaysStatsSummary(30)
|
||||||
|
.map { list ->
|
||||||
|
val reversed = list.reversed()
|
||||||
|
val keys = reversed.map { it.date.dayOfMonth.toString() }
|
||||||
|
val values = reversed.map { it.focusTime }
|
||||||
|
lastMonthSummary.first.runTransaction {
|
||||||
|
columnSeries { series(values) }
|
||||||
|
extras { it[lastMonthSummary.second] = keys }
|
||||||
}
|
}
|
||||||
}
|
lastMonthSummary
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
}
|
||||||
lastMonthAverageFocusTimes
|
.stateIn(
|
||||||
.collect {
|
scope = viewModelScope,
|
||||||
lastMonthSummaryAnalysisModelProducer.runTransaction {
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
columnSeries {
|
initialValue = lastMonthSummary
|
||||||
series(
|
)
|
||||||
it?.focusTimeQ1 ?: 0,
|
|
||||||
it?.focusTimeQ2 ?: 0,
|
val lastMonthAverageFocusTimes: StateFlow<List<Int>> =
|
||||||
it?.focusTimeQ3 ?: 0,
|
statRepository.getLastNDaysAverageFocusTimes(30)
|
||||||
it?.focusTimeQ4 ?: 0
|
.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)
|
||||||
|
)
|
||||||
|
|
||||||
|
val lastYearSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
|
||||||
|
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<List<Int>> =
|
||||||
|
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 {
|
companion object {
|
||||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||||
|
|||||||
@@ -56,4 +56,5 @@
|
|||||||
<string name="up_next">Up next</string>
|
<string name="up_next">Up next</string>
|
||||||
<string name="timer">Timer</string>
|
<string name="timer">Timer</string>
|
||||||
<string name="timer_progress">Timer progress</string>
|
<string name="timer_progress">Timer progress</string>
|
||||||
|
<string name="last_year">Last year</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user