refactor(stats): refactor heatmap for upcoming tooltip feature

This commit is contained in:
Nishant Mishra
2025-12-16 10:05:36 +05:30
parent 79851fb482
commit 85321352c0
4 changed files with 32 additions and 34 deletions

View File

@@ -17,6 +17,7 @@
package org.nsh07.pomodoro.data package org.nsh07.pomodoro.data
import androidx.compose.runtime.Immutable
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.time.LocalDate import java.time.LocalDate
@@ -26,6 +27,7 @@ import java.time.LocalDate
* durations for the 4 quarters of a day (00:00 - 12:00, 12:00 - 16:00, 16:00 - 20:00, 20:00 - 00:00) * durations for the 4 quarters of a day (00:00 - 12:00, 12:00 - 16:00, 16:00 - 20:00, 20:00 - 00:00)
* separately for later analysis (e.g. for showing which parts of the day are most productive). * separately for later analysis (e.g. for showing which parts of the day are most productive).
*/ */
@Immutable
@Entity(tableName = "stat") @Entity(tableName = "stat")
data class Stat( data class Stat(
@PrimaryKey @PrimaryKey

View File

@@ -59,9 +59,11 @@ import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastMaxBy import androidx.compose.ui.util.fastMaxBy
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.data.Stat
import org.nsh07.pomodoro.ui.theme.TomatoTheme import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
import org.nsh07.pomodoro.utils.millisecondsToMinutes import org.nsh07.pomodoro.utils.millisecondsToMinutes
import java.time.DayOfWeek
import java.time.LocalDate import java.time.LocalDate
import java.time.format.TextStyle import java.time.format.TextStyle
import java.util.Locale import java.util.Locale
@@ -271,45 +273,36 @@ val HEATMAP_CELL_SIZE = 28.dp
val HEATMAP_CELL_GAP = 2.dp val HEATMAP_CELL_GAP = 2.dp
/** /**
* A horizontally scrollable heatmap with week labels in the first column * A horizontally scrollable heatmap with persistent week labels in the first column
* *
* @param data Data to be represented in the heatmap in the form of [Pair]s of [LocalDate]s and * @param data Data to be represented in the heatmap as a [List] of [Stat] objects. A null value
* their corresponding focus durations as a list. A null value passed in the list can be used to * passed in the list can be used to insert gaps in the heatmap, and can be used to, for example,
* insert gaps in the heatmap, and can be used to, for example, delimit months by inserting an * delimit months by inserting a null week. Note that it is assumed that the dates are continuous
* empty week * (without gaps) and start with a Monday.
* @param modifier Modifier to be applied to the heatmap * @param modifier Modifier to be applied to the heatmap
* @param maxValue Maximum total value of the items present in [data]. This value must correspond to * @param maxValue Maximum total focus duration of the items present in [data]. This value must
* the sum of the list present in one of the elements on [data] for accurate representation. * correspond to the total focus duration one of the elements in [data] for accurate representation.
*
* Note that it is assumed that the dates are continuous (without gaps) and start with a Monday
*/ */
@Composable @Composable
fun HeatmapWithWeekLabels( fun HeatmapWithWeekLabels(
data: List<List<Long>?>, data: List<Stat?>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
size: Dp = HEATMAP_CELL_SIZE, size: Dp = HEATMAP_CELL_SIZE,
gap: Dp = HEATMAP_CELL_GAP, gap: Dp = HEATMAP_CELL_GAP,
contentPadding: PaddingValues = PaddingValues(), contentPadding: PaddingValues = PaddingValues(),
maxValue: Long = remember { data.fastMaxBy { it?.sum() ?: 0 }?.sum() ?: 0 }, maxValue: Long = remember {
data.fastMaxBy { it?.totalFocusTime() ?: 0 }?.totalFocusTime() ?: 0
},
) { ) {
val locale = Locale.getDefault() val locale = Locale.getDefault()
val shapes = shapes val shapes = shapes
val first7 = remember(locale) { val daysOfWeek = remember(locale) {
val monday = LocalDate.of(2024, 1, 1) // Monday DayOfWeek.entries.map {
it.getDisplayName(
buildList { TextStyle.NARROW,
repeat(7) { locale
add( )
monday
.plusDays(it.toLong())
.dayOfWeek
.getDisplayName(
TextStyle.NARROW,
locale
)
)
}
} }
} // Names of the 7 days of the week in the current locale } // Names of the 7 days of the week in the current locale
@@ -317,7 +310,7 @@ fun HeatmapWithWeekLabels(
Column( Column(
verticalArrangement = Arrangement.spacedBy(gap), verticalArrangement = Arrangement.spacedBy(gap),
) { ) {
first7.fastForEach { daysOfWeek.fastForEach {
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier.size(size) modifier = Modifier.size(size)
@@ -338,11 +331,13 @@ fun HeatmapWithWeekLabels(
verticalArrangement = Arrangement.spacedBy(gap), verticalArrangement = Arrangement.spacedBy(gap),
horizontalArrangement = Arrangement.spacedBy(gap) horizontalArrangement = Arrangement.spacedBy(gap)
) { ) {
itemsIndexed(data) { index, it -> itemsIndexed(
data,
key = { index, it -> it?.date?.toEpochDay() ?: index.toString() }) { index, it ->
if (it == null) { if (it == null) {
Spacer(Modifier.size(size)) Spacer(Modifier.size(size))
} else { } else {
val sum = remember { it.sum().toFloat() } val sum = remember { it.totalFocusTime().toFloat() }
val shape = remember { val shape = remember {
val top = data.getOrNull(index - 1) != null && index % 7 != 0 val top = data.getOrNull(index - 1) != null && index % 7 != 0
@@ -411,12 +406,12 @@ fun HeatmapWithWeekLabelsPreview() {
buildList { buildList {
(0..93).forEach { index -> (0..93).forEach { index ->
val date = startDate.plusDays(index.toLong()) val date = startDate.plusDays(index.toLong())
val focusDurations = listOf(index % 10L / 2) // Varying focus durations val focusStat = Stat(date, index % 10L / 2, 0, 0, 0, 0) // Varying focus durations
if (date.month != date.minusDays(1).month && index > 0) if (date.month != date.minusDays(1).month && index > 0)
repeat(7) { add(null) } repeat(7) { add(null) }
add(focusDurations) add(focusStat)
} }
} }
} }

View File

@@ -69,6 +69,7 @@ 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 org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.data.Stat
import org.nsh07.pomodoro.ui.mergePaddingValues import org.nsh07.pomodoro.ui.mergePaddingValues
import org.nsh07.pomodoro.ui.statsScreen.components.FocusBreakRatioVisualization import org.nsh07.pomodoro.ui.statsScreen.components.FocusBreakRatioVisualization
import org.nsh07.pomodoro.ui.statsScreen.components.FocusBreakdownChart import org.nsh07.pomodoro.ui.statsScreen.components.FocusBreakdownChart
@@ -88,7 +89,7 @@ import org.nsh07.pomodoro.utils.millisecondsToMinutes
fun SharedTransitionScope.LastYearScreen( fun SharedTransitionScope.LastYearScreen(
contentPadding: PaddingValues, contentPadding: PaddingValues,
focusBreakdownValues: Pair<List<Long>, Long>, focusBreakdownValues: Pair<List<Long>, Long>,
focusHeatmapData: List<List<Long>?>, focusHeatmapData: List<Stat?>,
heatmapMaxValue: Long, heatmapMaxValue: Long,
mainChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>, mainChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
onBack: () -> Unit, onBack: () -> Unit,

View File

@@ -206,7 +206,7 @@ class StatsViewModel(
initialValue = lastYearSummary initialValue = lastYearSummary
) )
val lastYearFocusHeatmapData: StateFlow<List<List<Long>?>> = val lastYearFocusHeatmapData: StateFlow<List<Stat?>> =
lastYearStatsFlow lastYearStatsFlow
.map { list -> .map { list ->
val list = list.reversed() val list = list.reversed()
@@ -224,7 +224,7 @@ class StatsViewModel(
repeat(7) { add(null) } // Add a week gap if a new month starts repeat(7) { add(null) } // Add a week gap if a new month starts
} }
with(list[it]) { with(list[it]) {
add(listOf(focusTimeQ1, focusTimeQ2, focusTimeQ3, focusTimeQ4)) add(list[it])
} }
} }
} }