feat(stats): add a variable width 1D heatmap visualization

This commit is contained in:
Nishant Mishra
2025-12-11 22:26:00 +05:30
parent 6cb21b94a0
commit 38014f4d99

View File

@@ -0,0 +1,117 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.statsScreen.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import org.nsh07.pomodoro.ui.theme.TomatoTheme
/**
* A custom implementation of the 1-Dimensional heatmap plot that varies the width of the cells
* instead of the colors. The colors are varied according to the `maxIndex` value passed but they do
* NOT correspond to the actual values represented by the cells, and exist for aesthetic reasons
* only.
*/
@Composable
fun VariableWidth1DHeatmap(
values: List<Long>,
modifier: Modifier = Modifier,
rankList: List<Int> = remember(values) {
val sortedIndices = values.indices.sortedByDescending { values[it] }
val ranks = MutableList(values.size) { 0 }
sortedIndices.forEachIndexed { rank, originalIndex ->
ranks[originalIndex] = rank
}
ranks
},
height: Dp = 40.dp,
gap: Dp = 2.dp
) {
val firstNonZeroIndex = remember(values) { values.indexOfFirst { it > 0L } }
val lastNonZeroIndex = remember(values) { values.indexOfLast { it > 0L } }
Row(
horizontalArrangement = Arrangement.spacedBy(gap),
modifier = modifier
) {
values.fastForEachIndexed { index, item ->
if (item > 0L) {
val shape = when (index) {
firstNonZeroIndex -> shapes.large.copy(
topEnd = shapes.extraSmall.topEnd,
bottomEnd = shapes.extraSmall.bottomEnd
)
lastNonZeroIndex -> shapes.large.copy(
topStart = shapes.extraSmall.topStart,
bottomStart = shapes.extraSmall.bottomStart
)
else -> shapes.extraSmall
}
Spacer(
Modifier
.weight(item.toFloat())
.height(height)
.clip(shape)
.background(colorScheme.primaryContainer)
.background(
colorScheme.primary.copy(
(1f - (rankList.getOrNull(index) ?: 0) * 0.1f).coerceAtLeast(0.1f)
)
)
)
}
}
}
}
@Preview
@Composable
fun VariableWidth1DHeatmapPreview() {
val values = listOf(38L, 190L, 114L, 14L)
val rankList = listOf(2, 0, 1, 3)
TomatoTheme(dynamicColor = false) {
Surface {
VariableWidth1DHeatmap(
values = values,
rankList = rankList,
modifier = Modifier.padding(16.dp),
height = 40.dp,
gap = 2.dp,
)
}
}
}