From 06a5677324114392a47260c8d5a60d230564d28e Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Mon, 17 Nov 2025 20:11:20 +0300 Subject: [PATCH] Implement timeline chart for daily cigarette tracking - Added a new timeline chart to visualize cigarette consumption throughout the day. - Introduced average consumption calculations by hour using historical data. - Implemented date selection for users to view specific day's statistics. - Enhanced UI with responsive design elements and improved data presentation. - Included markers for individual cigarette instances on the chart for better insights. --- src/pages/tracker/tracker.tsx | 278 +++++++++++++++++++++++++++++++++- 1 file changed, 277 insertions(+), 1 deletion(-) diff --git a/src/pages/tracker/tracker.tsx b/src/pages/tracker/tracker.tsx index 110e419..6cf0317 100644 --- a/src/pages/tracker/tracker.tsx +++ b/src/pages/tracker/tracker.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useMemo } from 'react' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' @@ -14,7 +14,19 @@ import { Card, Textarea, Stack, + Flex, } from '@chakra-ui/react' +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + ReferenceDot, +} from 'recharts' +import { format, startOfDay, endOfDay } from 'date-fns' import { Field } from '../../components/ui/field' import { cigarettesApi } from '../../api/client' import { URLs } from '../../__data__/urls' @@ -32,6 +44,7 @@ export const TrackerPage: React.FC = () => { const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) + const [selectedDate, setSelectedDate] = useState(format(new Date(), 'yyyy-MM-dd')) const { register, @@ -113,6 +126,121 @@ export const TrackerPage: React.FC = () => { }) } + // Calculate average consumption by hour from all historical data + const averageByHour = useMemo(() => { + // Group all cigarettes by date and hour + const dateHourMap: Record> = {} + + cigarettes.forEach((cig) => { + const cigDate = new Date(cig.smokedAt) + const dateKey = format(cigDate, 'yyyy-MM-dd') + const hour = cigDate.getHours() + + if (!dateHourMap[dateKey]) { + dateHourMap[dateKey] = {} + } + + // Count cumulative cigarettes up to this hour for this day + for (let h = hour; h <= 24; h++) { + dateHourMap[dateKey][h] = (dateHourMap[dateKey][h] || 0) + 1 + } + }) + + // Calculate average for each hour + const hourAverages: Record = {} + const daysCount = Object.keys(dateHourMap).length || 1 + + for (let hour = 8; hour <= 24; hour++) { + let totalForHour = 0 + Object.values(dateHourMap).forEach((dayData) => { + totalForHour += dayData[hour] || 0 + }) + hourAverages[hour] = totalForHour / daysCount + } + + return hourAverages + }, [cigarettes]) + + // Prepare timeline data for the selected date + const timelineData = useMemo(() => { + const targetDate = new Date(selectedDate) + const dayStart = startOfDay(targetDate) + const dayEnd = endOfDay(targetDate) + + // Filter cigarettes for the selected date + const dayCigarettes = cigarettes.filter((cig) => { + const cigDate = new Date(cig.smokedAt) + return cigDate >= dayStart && cigDate <= dayEnd + }) + + // Sort by time + dayCigarettes.sort((a, b) => new Date(a.smokedAt).getTime() - new Date(b.smokedAt).getTime()) + + // Create timeline from 8:00 to 24:00 + const timeline: Array<{ + time: string + hour: number + minute: number + count: number + averageCount: number + cigarettes: Cigarette[] + }> = [] + + for (let hour = 8; hour <= 24; hour++) { + for (let minute = 0; minute < 60; minute += 30) { + if (hour === 24 && minute > 0) break // Stop at 24:00 + + const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}` + const targetTime = new Date(targetDate) + targetTime.setHours(hour, minute, 0, 0) + + // Count cigarettes up to this time + const cigsUpToNow = dayCigarettes.filter((cig) => { + const cigTime = new Date(cig.smokedAt) + return cigTime <= targetTime + }) + + // Get average count for this hour (interpolate for half hours) + const avgCount = minute === 0 + ? averageByHour[hour] || 0 + : ((averageByHour[hour] || 0) + (averageByHour[hour + 1] || 0)) / 2 + + timeline.push({ + time: timeStr, + hour, + minute, + count: cigsUpToNow.length, + averageCount: avgCount, + cigarettes: cigsUpToNow, + }) + } + } + + return { timeline, dayCigarettes } + }, [cigarettes, selectedDate, averageByHour]) + + // Get cigarette markers for the chart + const cigaretteMarkers = useMemo(() => { + return timelineData.dayCigarettes.map((cig) => { + const cigDate = new Date(cig.smokedAt) + const hours = cigDate.getHours() + const minutes = cigDate.getMinutes() + const timeDecimal = hours + minutes / 60 + + // Find the count at this time + const count = timelineData.dayCigarettes.filter((c) => { + return new Date(c.smokedAt) <= cigDate + }).length + + return { + time: format(cigDate, 'HH:mm'), + timeDecimal, + count, + note: cig.note, + } + }) + }, [timelineData.dayCigarettes]) + return ( @@ -217,6 +345,154 @@ export const TrackerPage: React.FC = () => { )} + {/* Timeline Chart */} + + + + + График за день + + + Дата: + + setSelectedDate(e.target.value)} + maxW="200px" + size="sm" + /> + + + + {timelineData.dayCigarettes.length === 0 ? ( + + + За выбранный день записей нет + + + ) : ( + <> + + + Всего за день: {timelineData.dayCigarettes.length} {timelineData.dayCigarettes.length === 1 ? 'сигарета' : 'сигарет'} + + + + + + + + { + // Show only full hours + if (value.endsWith(':00')) { + return value + } + return '' + }} + /> + + { + if (active && payload && payload.length) { + const data = payload[0].payload + return ( + + + {data.time} + + + Сегодня: {data.count} + + + Среднее: {data.averageCount.toFixed(1)} + + + ) + } + return null + }} + /> + {/* Average line */} + + {/* Current day line */} + + {/* Add markers for each cigarette */} + {cigaretteMarkers.map((marker, idx) => ( + + ))} + + + + + {/* Legend */} + + + + Сегодня + + + + Среднее + + + + Момент курения + + + + )} + + + + {/* Recent cigarettes list */}