From 7e91e08532e9400fff2964ff349a95b39cd06a2d Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Thu, 12 Mar 2026 00:33:56 +0100 Subject: [PATCH] feat: 2-hour bucket grid for larger square cells --- components/dashboard/PeakHours.tsx | 90 +++++++++++++++++------------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/components/dashboard/PeakHours.tsx b/components/dashboard/PeakHours.tsx index 7850305..47d51b4 100644 --- a/components/dashboard/PeakHours.tsx +++ b/components/dashboard/PeakHours.tsx @@ -12,9 +12,10 @@ interface PeakHoursProps { } const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] -const HOUR_LABELS: Record = { 0: '12am', 6: '6am', 12: '12pm', 18: '6pm' } +const BUCKETS = 12 // 2-hour buckets +// Label at bucket index 0=12am, 3=6am, 6=12pm, 9=6pm +const BUCKET_LABELS: Record = { 0: '12am', 3: '6am', 6: '12pm', 9: '6pm' } -// Orange intensity palette (light → dark) const HIGHLIGHT_COLORS = [ 'rgba(253,94,15,0.18)', 'rgba(253,94,15,0.38)', @@ -22,6 +23,13 @@ const HIGHLIGHT_COLORS = [ '#FD5E0F', ] +function formatBucket(bucket: number): string { + const hour = bucket * 2 + if (hour === 0) return '12am–2am' + if (hour === 12) return '12pm–2pm' + return hour < 12 ? `${hour}am–${hour + 2}am` : `${hour - 12}pm–${hour - 10}pm` +} + function formatHour(hour: number): string { if (hour === 0) return '12am' if (hour === 12) return '12pm' @@ -40,7 +48,7 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) { const [data, setData] = useState([]) const [isLoading, setIsLoading] = useState(true) const [animKey, setAnimKey] = useState(0) - const [hovered, setHovered] = useState<{ day: number; hour: number } | null>(null) + const [hovered, setHovered] = useState<{ day: number; bucket: number } | null>(null) const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>(null) const gridRef = useRef(null) @@ -60,53 +68,55 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) { fetchData() }, [siteId, dateRange]) - const { grid, max, dayTotals, hourTotals, weekTotal } = useMemo(() => { - const grid: number[][] = Array.from({ length: 7 }, () => Array(24).fill(0)) + const { grid, max, dayTotals, bucketTotals, weekTotal } = useMemo(() => { + // grid[day][bucket] — aggregate 2-hour buckets + const grid: number[][] = Array.from({ length: 7 }, () => Array(BUCKETS).fill(0)) for (const d of data) { const date = new Date(d.date) const day = date.getDay() const hour = date.getHours() const adjustedDay = day === 0 ? 6 : day - 1 - grid[adjustedDay][hour] += d.pageviews + const bucket = Math.floor(hour / 2) + grid[adjustedDay][bucket] += d.pageviews } const max = Math.max(...grid.flat(), 1) - const dayTotals = grid.map(hours => hours.reduce((a, b) => a + b, 0)) - const hourTotals = Array.from({ length: 24 }, (_, h) => grid.reduce((a, row) => a + row[h], 0)) + const dayTotals = grid.map(buckets => buckets.reduce((a, b) => a + b, 0)) + const bucketTotals = Array.from({ length: BUCKETS }, (_, b) => grid.reduce((a, row) => a + row[b], 0)) const weekTotal = dayTotals.reduce((a, b) => a + b, 0) - return { grid, max, dayTotals, hourTotals, weekTotal } + return { grid, max, dayTotals, bucketTotals, weekTotal } }, [data]) const hasData = data.some(d => d.pageviews > 0) const bestTime = useMemo(() => { if (!hasData) return null - let bestDay = 0, bestHour = 0, bestVal = 0 + let bestDay = 0, bestBucket = 0, bestVal = 0 for (let d = 0; d < 7; d++) { - for (let h = 0; h < 24; h++) { - if (grid[d][h] > bestVal) { - bestVal = grid[d][h] + for (let b = 0; b < BUCKETS; b++) { + if (grid[d][b] > bestVal) { + bestVal = grid[d][b] bestDay = d - bestHour = h + bestBucket = b } } } - return { day: bestDay, hour: bestHour } + return { day: bestDay, bucket: bestBucket } }, [grid, hasData]) const tooltipData = useMemo(() => { if (!hovered) return null - const { day, hour } = hovered - const value = grid[day][hour] + const { day, bucket } = hovered + const value = grid[day][bucket] const pct = weekTotal > 0 ? Math.round((value / weekTotal) * 100) : 0 - return { value, dayTotal: dayTotals[day], hourTotal: hourTotals[hour], pct } - }, [hovered, grid, dayTotals, hourTotals, weekTotal]) + return { value, dayTotal: dayTotals[day], bucketTotal: bucketTotals[bucket], pct } + }, [hovered, grid, dayTotals, bucketTotals, weekTotal]) const handleCellMouseEnter = ( e: React.MouseEvent, dayIdx: number, - hour: number + bucket: number ) => { - setHovered({ day: dayIdx, hour }) + setHovered({ day: dayIdx, bucket }) if (gridRef.current) { const gridRect = gridRef.current.getBoundingClientRect() const cellRect = (e.currentTarget as HTMLDivElement).getBoundingClientRect() @@ -138,36 +148,38 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) { ) : hasData ? ( <>
- {grid.map((hours, dayIdx) => ( + {grid.map((buckets, dayIdx) => (
{DAYS[dayIdx]} -
- {hours.map((value, hour) => { - const isHoveredCell = hovered?.day === dayIdx && hovered?.hour === hour - const isBestCell = bestTime?.day === dayIdx && bestTime?.hour === hour +
+ {buckets.map((value, bucket) => { + const isHoveredCell = hovered?.day === dayIdx && hovered?.bucket === bucket + const isBestCell = bestTime?.day === dayIdx && bestTime?.bucket === bucket const isActive = value > 0 const highlightColor = getHighlightColor(value, max) return (
handleCellMouseEnter(e, dayIdx, hour)} + onMouseEnter={(e) => handleCellMouseEnter(e, dayIdx, bucket)} onMouseLeave={() => { setHovered(null); setTooltipPos(null) }} /> ) @@ -180,11 +192,11 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) {
- {Object.entries(HOUR_LABELS).map(([h, label]) => ( + {Object.entries(BUCKET_LABELS).map(([b, label]) => ( {label} @@ -216,7 +228,7 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) { >
- {DAYS[hovered.day]} {formatHour(hovered.hour)} + {DAYS[hovered.day]} {formatBucket(hovered.bucket)}
{tooltipData.value.toLocaleString()} pageviews @@ -237,12 +249,12 @@ export default function PeakHours({ siteId, dateRange }: PeakHoursProps) { Your busiest time is{' '} - {DAYS[bestTime.day]}s at {formatHour(bestTime.hour)} + {DAYS[bestTime.day]}s at {formatHour(bestTime.bucket * 2)} )}