diff --git a/apps/mobile/src/components/BarChart.tsx b/apps/mobile/src/components/BarChart.tsx index d5b8a76..3041187 100644 --- a/apps/mobile/src/components/BarChart.tsx +++ b/apps/mobile/src/components/BarChart.tsx @@ -63,9 +63,6 @@ export function BarChart({ const { disableScroll, enableScroll } = useScreenScrollControl() - const today = new Date() - today.setHours(0, 0, 0, 0) - // Find oldest date from data or events const oldestDate = React.useMemo(() => { const candidatesForOldestDate: Date[] = [] @@ -109,138 +106,146 @@ export function BarChart({ }, [data, events]) // Generate bars data (including empty bars) - const bars: Bar[] = [] - if (timeRange === 'Year') { - // For year view, group all data by month+year and sum values - - // Find the current month - const currentMonth = new Date(today.getFullYear(), today.getMonth(), 1) - currentMonth.setHours(0, 0, 0, 0) - - // Calculate total months from first month to current month - const totalMonths = (currentMonth.getFullYear() - oldestDate.getFullYear()) * 12 + - (currentMonth.getMonth() - oldestDate.getMonth()) + 1 - - // Determine the start month - backfill if needed to ensure at least 12 bars - // Always end at current month, backfill empty months before first month if needed - let startMonth: Date - if (totalMonths < 12) { - // Backfill empty months before the first month to reach 12 bars total - const monthsToBackfill = 12 - totalMonths - startMonth = new Date(oldestDate) - startMonth.setMonth(startMonth.getMonth() - monthsToBackfill) - startMonth.setDate(1) - startMonth.setHours(0, 0, 0, 0) - } else { - startMonth = new Date(oldestDate) - } + // Memoize bars to prevent unnecessary re-renders and scroll resets + const bars = React.useMemo(() => { + const today = new Date() + today.setHours(0, 0, 0, 0) + + const bars: Bar[] = [] + if (timeRange === 'Year') { + // For year view, group all data by month+year and sum values + + // Find the current month + const currentMonth = new Date(today.getFullYear(), today.getMonth(), 1) + currentMonth.setHours(0, 0, 0, 0) + + // Calculate total months from first month to current month + const totalMonths = (currentMonth.getFullYear() - oldestDate.getFullYear()) * 12 + + (currentMonth.getMonth() - oldestDate.getMonth()) + 1 + + // Determine the start month - backfill if needed to ensure at least 12 bars + // Always end at current month, backfill empty months before first month if needed + let startMonth: Date + if (totalMonths < 12) { + // Backfill empty months before the first month to reach 12 bars total + const monthsToBackfill = 12 - totalMonths + startMonth = new Date(oldestDate) + startMonth.setMonth(startMonth.getMonth() - monthsToBackfill) + startMonth.setDate(1) + startMonth.setHours(0, 0, 0, 0) + } else { + startMonth = new Date(oldestDate) + } - // Loop through all months from start month to current month (always at least 12) - const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - const currentMonthDate = new Date(startMonth) + // Loop through all months from start month to current month (always at least 12) + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + const currentMonthDate = new Date(startMonth) - while (currentMonthDate <= currentMonth) { - const monthYear = currentMonthDate.getFullYear() - const monthIndex = currentMonthDate.getMonth() + while (currentMonthDate <= currentMonth) { + const monthYear = currentMonthDate.getFullYear() + const monthIndex = currentMonthDate.getMonth() - let monthValue = 0 - // Find all data points in this month+year and sum their values - const monthData = data.filter(d => { - const dDate = new Date(d.date) - dDate.setHours(0, 0, 0, 0) - return dDate.getFullYear() === monthYear && dDate.getMonth() === monthIndex - }) + let monthValue = 0 + // Find all data points in this month+year and sum their values + const monthData = data.filter(d => { + const dDate = new Date(d.date) + dDate.setHours(0, 0, 0, 0) + return dDate.getFullYear() === monthYear && dDate.getMonth() === monthIndex + }) - monthValue = monthData.reduce((sum, d) => sum + d.value, 0) + monthValue = monthData.reduce((sum, d) => sum + d.value, 0) - // Collect log entries for this month - const monthLogs: Array = [] - monthData.forEach(d => { - if (d.logs) { - monthLogs.push(...d.logs) - } - }) + // Collect log entries for this month + const monthLogs: Array = [] + monthData.forEach(d => { + if (d.logs) { + monthLogs.push(...d.logs) + } + }) - bars.push({ - label: monthNames[monthIndex], - value: monthValue, - date: new Date(currentMonthDate), - logs: monthLogs.length > 0 ? monthLogs : undefined, - }) + bars.push({ + label: monthNames[monthIndex], + value: monthValue, + date: new Date(currentMonthDate), + logs: monthLogs.length > 0 ? monthLogs : undefined, + }) - // Move to next month - currentMonthDate.setMonth(currentMonthDate.getMonth() + 1) - } - } else { - // For week and month views, iterate day by day - const msInDay = 24 * 60 * 60 * 1000 + // Move to next month + currentMonthDate.setMonth(currentMonthDate.getMonth() + 1) + } + } else { + // For week and month views, iterate day by day + const msInDay = 24 * 60 * 60 * 1000 - const normalizedOldestDate = new Date(oldestDate) - normalizedOldestDate.setHours(0, 0, 0, 0) + const normalizedOldestDate = new Date(oldestDate) + normalizedOldestDate.setHours(0, 0, 0, 0) - const normalizedNewestDate = new Date(newestDate) - normalizedNewestDate.setHours(0, 0, 0, 0) + const normalizedNewestDate = new Date(newestDate) + normalizedNewestDate.setHours(0, 0, 0, 0) - // Like Year view, always show up through "today" (unless there is future data/events). - const endDate = new Date(Math.max(today.getTime(), normalizedNewestDate.getTime())) - endDate.setHours(0, 0, 0, 0) + // Like Year view, always show up through "today" (unless there is future data/events). + const endDate = new Date(Math.max(today.getTime(), normalizedNewestDate.getTime())) + endDate.setHours(0, 0, 0, 0) - const minBarsForRange = timeRange === 'Week' ? 7 : 30 + const minBarsForRange = timeRange === 'Week' ? 7 : 30 - // Clamp start so we don't get negative ranges (e.g. future-only data). - const naturalStartDate = new Date(Math.min(normalizedOldestDate.getTime(), endDate.getTime())) - naturalStartDate.setHours(0, 0, 0, 0) + // Clamp start so we don't get negative ranges (e.g. future-only data). + const naturalStartDate = new Date(Math.min(normalizedOldestDate.getTime(), endDate.getTime())) + naturalStartDate.setHours(0, 0, 0, 0) - const totalDays = Math.floor((endDate.getTime() - naturalStartDate.getTime()) / msInDay) + 1 + const totalDays = Math.floor((endDate.getTime() - naturalStartDate.getTime()) / msInDay) + 1 - // Backfill empty days before the first day to ensure a minimum bar count. - const startDate = new Date(naturalStartDate) - if (totalDays < minBarsForRange) { - const daysToBackfill = minBarsForRange - totalDays - startDate.setDate(startDate.getDate() - daysToBackfill) - } + // Backfill empty days before the first day to ensure a minimum bar count. + const startDate = new Date(naturalStartDate) + if (totalDays < minBarsForRange) { + const daysToBackfill = minBarsForRange - totalDays + startDate.setDate(startDate.getDate() - daysToBackfill) + } - const currentDate = new Date(startDate) - while (currentDate <= endDate) { - const existingData = data.find(d => { - const dDate = new Date(d.date) - dDate.setHours(0, 0, 0, 0) + const currentDate = new Date(startDate) + while (currentDate <= endDate) { + const existingData = data.find(d => { + const dDate = new Date(d.date) + dDate.setHours(0, 0, 0, 0) - return dDate.getTime() === currentDate.getTime() - }) + return dDate.getTime() === currentDate.getTime() + }) - let label = '' - if (timeRange === 'Week') { - const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] - label = `${dayNames[currentDate.getDay()]} ${currentDate.getDate()}` - } else { - // Month - if (currentDate.getDate() % 5 !== 0) { - label = '' + let label = '' + if (timeRange === 'Week') { + const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + label = `${dayNames[currentDate.getDay()]} ${currentDate.getDate()}` } else { - label = `${currentDate.getMonth() + 1}/${currentDate.getDate()}` + // Month + if (currentDate.getDate() % 5 !== 0) { + label = '' + } else { + label = `${currentDate.getMonth() + 1}/${currentDate.getDate()}` + } } - } - bars.push({ - label, - value: existingData ? existingData.value : 0, - date: new Date(currentDate), - logs: existingData?.logs, - }) + bars.push({ + label, + value: existingData ? existingData.value : 0, + date: new Date(currentDate), + logs: existingData?.logs, + }) + + currentDate.setDate(currentDate.getDate() + 1) + } + } - currentDate.setDate(currentDate.getDate() + 1) + // Limit to latest X bars to prevent performance issues + if (bars.length > MAX_BARS) { + bars.splice(0, bars.length - MAX_BARS) } - } - // Limit to latest X bars to prevent performance issues - if (bars.length > MAX_BARS) { - bars.splice(0, bars.length - MAX_BARS) - } + return bars + }, [data, events, timeRange, oldestDate, newestDate]) const numBarsInView = timeRange === 'Year' ? 12 : timeRange === 'Week' ? 7 : 30 const availableWidth = CHART_WIDTH - (numBarsInView * BAR_SPACING) - const barWidth = Math.max(availableWidth / numBarsInView, 4) - (showYAxisLabels ? 2 : 0) + const barWidth = React.useMemo(() => Math.max(availableWidth / numBarsInView, 4) - (showYAxisLabels ? 2 : 0), [numBarsInView, showYAxisLabels]) const maxValue = Math.max(...bars.map(d => d.value), 1) const chartHeight = height - (showXAxisLabels ? LABEL_HEIGHT : 0) - (events.length > 0 ? INFO_ICON_HEIGHT : 0)