@@ -11,9 +11,12 @@ import androidx.compose.ui.geometry.CornerRadius
1111import androidx.compose.ui.geometry.Offset
1212import androidx.compose.ui.geometry.Size
1313import androidx.compose.ui.graphics.PathEffect
14+ import androidx.compose.ui.text.TextStyle
1415import androidx.compose.ui.text.drawText
16+ import androidx.compose.ui.text.font.FontWeight
1517import androidx.compose.ui.text.rememberTextMeasurer
1618import androidx.compose.ui.unit.dp
19+ import androidx.compose.ui.unit.sp
1720import neth.iecal.trease.models.FocusStats
1821import neth.iecal.trease.utils.getHourOfDay
1922import kotlin.math.ceil
@@ -30,120 +33,144 @@ fun HourlyFocusChart(stats: List<FocusStats>) {
3033 buckets.toList()
3134 }
3235
33- Column (Modifier .fillMaxSize()) {
34- Text (
35- text = " Productivity by Hour" ,
36- style = MaterialTheme .typography.titleMedium,
37- color = MaterialTheme .colorScheme.onSurface,
38- modifier = Modifier .padding(bottom = 24 .dp).align(Alignment .CenterHorizontally )
39- )
36+ BoxWithConstraints (Modifier .fillMaxSize()) {
37+ val isCompact = maxHeight < 180 .dp || maxWidth < 260 .dp
38+
39+ Column (Modifier .fillMaxSize()) {
40+ val headerStyle = if (isCompact) MaterialTheme .typography.labelSmall else MaterialTheme .typography.titleMedium
41+ val headerPadding = if (isCompact) 4 .dp else 24 .dp
42+ val headerText = if (isCompact) " Peak Hours" else " Productivity by Hour"
43+
44+ Text (
45+ text = headerText,
46+ style = headerStyle,
47+ color = MaterialTheme .colorScheme.onSurface,
48+ modifier = Modifier
49+ .padding(bottom = headerPadding)
50+ .align(Alignment .CenterHorizontally )
51+ )
4052
41- HourlyBarChartCanvas (hourlyData)
53+ HourlyBarChartCanvas (
54+ data = hourlyData,
55+ isCompact = isCompact
56+ )
57+ }
4258 }
4359}
4460
4561@Composable
46- fun HourlyBarChartCanvas (data : List <Long >) {
62+ fun HourlyBarChartCanvas (
63+ data : List <Long >,
64+ isCompact : Boolean
65+ ) {
4766 val textMeasurer = rememberTextMeasurer()
4867
4968 val barColor = MaterialTheme .colorScheme.primary
5069 val trackColor = MaterialTheme .colorScheme.surfaceContainerHigh
51- val gridColor = MaterialTheme .colorScheme.outlineVariant
70+ val gridColor = MaterialTheme .colorScheme.outlineVariant.copy(alpha = 0.5f )
5271 val labelColor = MaterialTheme .colorScheme.onSurfaceVariant
53- val labelStyle = MaterialTheme .typography.labelSmall
5472
55- Canvas (
56- modifier = Modifier
57- .fillMaxSize()
58- .padding(start = 40 .dp, end = 16 .dp, top = 16 .dp, bottom = 32 .dp)
59- ) {
60- if (size.width <= 0 || size.height <= 0 ) return @Canvas
73+ val labelStyle = if (isCompact)
74+ TextStyle (fontSize = 8 .sp, fontWeight = FontWeight .Medium )
75+ else
76+ MaterialTheme .typography.labelSmall
6177
62- val maxVal = data.maxOfOrNull { it } ? : 0L
78+ val leftPadding = if (isCompact) 24 .dp else 40 .dp
79+ val bottomPadding = if (isCompact) 16 .dp else 32 .dp
80+ val barCornerRadius = if (isCompact) 1.5 .dp else 3 .dp
6381
64- val yMax = max( 60L , (ceil(maxVal / 60.0 ) * 60 ).toLong())
82+ val gridSteps = if (isCompact) 2 else 4
6583
66- val chartHeight = size.height
67- val chartWidth = size.width
84+ Canvas (modifier = Modifier .fillMaxSize()) {
85+ val width = size.width
86+ val height = size.height
6887
88+ val leftPadPx = leftPadding.toPx()
89+ val botPadPx = bottomPadding.toPx()
6990
70- val barWidth = (chartWidth / 24 ) * 0.6f
71- val spacing = (chartWidth / 24 )
91+ val chartWidth = width - leftPadPx
92+ val chartHeight = height - botPadPx
93+
94+ if (chartWidth <= 0 || chartHeight <= 0 ) return @Canvas
95+
96+ val maxVal = data.maxOfOrNull { it } ? : 0L
97+ val yMax = max(60L , (ceil(maxVal / 60.0 ) * 60 ).toLong())
7298
73- val steps = 4
74- val stepHeight = chartHeight / steps
75- val stepValue = yMax / steps
99+ val stepHeight = chartHeight / gridSteps
100+ val stepValue = yMax / gridSteps
76101
77- for (i in 0 .. steps ) {
102+ for (i in 0 .. gridSteps ) {
78103 val y = chartHeight - (i * stepHeight)
79104 val value = i * stepValue
80105
81-
82106 drawLine(
83107 color = gridColor,
84- start = Offset (0f , y),
85- end = Offset (chartWidth , y),
108+ start = Offset (leftPadPx , y),
109+ end = Offset (width , y),
86110 strokeWidth = 1 .dp.toPx(),
87111 pathEffect = PathEffect .dashPathEffect(floatArrayOf(10f , 10f ))
88112 )
89113
90- // Y-Label
91114 if (i > 0 ) {
92115 val label = if (value >= 60 ) " ${value / 60 } h" else " ${value} m"
93116
94- val textLayoutResult = textMeasurer.measure(
95- text = label,
96- style = labelStyle.copy(color = labelColor)
97- )
117+ val textResult = textMeasurer.measure(label, labelStyle.copy(color = labelColor))
98118
99119 drawText(
100- textLayoutResult = textLayoutResult ,
120+ textLayoutResult = textResult ,
101121 topLeft = Offset (
102- x = - 35 .dp.toPx(),
103- y = y - (textLayoutResult .size.height / 2 )
122+ x = (leftPadPx - textResult.size.width) - 4 .dp.toPx(),
123+ y = y - (textResult .size.height / 2 )
104124 )
105125 )
106126 }
107127 }
108128
129+ val barSpaceAllocated = chartWidth / 24
130+ val fillRatio = if (isCompact) 0.75f else 0.6f
131+ val barWidth = barSpaceAllocated * fillRatio
132+ val halfBarWidth = barWidth / 2
109133
110134 data.forEachIndexed { hour, minutes ->
111- val x = (hour * spacing ) + (spacing / 2 )
135+ val centerX = leftPadPx + (hour * barSpaceAllocated ) + (barSpaceAllocated / 2 )
112136 val barHeight = (minutes / yMax.toFloat()) * chartHeight
113137
114- // Track (Background Bar)
115138 drawRoundRect(
116139 color = trackColor.copy(alpha = 0.3f ),
117- topLeft = Offset (x - barWidth / 2 , 0f ),
140+ topLeft = Offset (centerX - halfBarWidth , 0f ),
118141 size = Size (barWidth, chartHeight),
119- cornerRadius = CornerRadius (2 .dp. toPx()) // Slightly sharper corners for thinner bars
142+ cornerRadius = CornerRadius (barCornerRadius. toPx())
120143 )
121144
122- // Active Bar
123- drawRoundRect(
124- color = barColor,
125- topLeft = Offset (x - barWidth / 2 , chartHeight - barHeight),
126- size = Size (barWidth, barHeight),
127- cornerRadius = CornerRadius (2 .dp.toPx())
128- )
145+ if (barHeight > 0 ) {
146+ drawRoundRect(
147+ color = barColor,
148+ topLeft = Offset (centerX - halfBarWidth, chartHeight - barHeight),
149+ size = Size (barWidth, barHeight),
150+ cornerRadius = CornerRadius (barCornerRadius.toPx())
151+ )
152+ }
129153
154+ // Show every 6 hours (0, 6, 12, 18) to prevent clutter
130155 if (hour % 6 == 0 ) {
131156 val timeLabel = when (hour) {
132157 0 -> " 12am"
133158 12 -> " 12pm"
134- else -> " ${hour % 12 }${if (hour < 12 ) " am" else " pm" } "
159+ else -> " ${hour % 12 }${if (isCompact) " " else if (hour < 12 ) " am" else " pm" } "
160+ // In compact mode, just show "6", "12" etc to save space, or keep AM/PM if it fits.
161+ // Let's keep it simple: "6" for compact, "6am" for full
135162 }
136163
137- val textLayoutResult = textMeasurer.measure(
138- text = timeLabel,
139- style = labelStyle.copy(color = labelColor)
140- )
164+ // If compact, simplify text further
165+ val finalLabel = if (isCompact && timeLabel.length > 2 ) timeLabel.filter { it.isDigit() } else timeLabel
166+
167+ val textResult = textMeasurer.measure(finalLabel, labelStyle.copy(color = labelColor) )
141168
142169 drawText(
143- textLayoutResult = textLayoutResult ,
170+ textLayoutResult = textResult ,
144171 topLeft = Offset (
145- x = x - (textLayoutResult .size.width / 2 ),
146- y = chartHeight + 8 .dp.toPx()
172+ x = centerX - (textResult .size.width / 2 ),
173+ y = chartHeight + 6 .dp.toPx()
147174 )
148175 )
149176 }
0 commit comments