import type { PostAnalytics, DayOfWeekMetrics, HeatmapReport } from "../models/types.js"; const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; // Convert JS getDay() (0=Sun) to ISO weekday (1=Mon, 7=Sun) function toISOWeekday(jsDay: number): number { return jsDay === 0 ? 7 : jsDay; } /** * Generate a day-of-week performance heatmap from post analytics data. * Groups posts by day of week and calculates average metrics per day. */ export function generateHeatmap(posts: PostAnalytics[]): HeatmapReport { // Initialize buckets for all 7 days (ISO: 1=Mon to 7=Sun) const buckets: Map = new Map(); for (let i = 1; i <= 7; i++) { buckets.set(i, []); } // Group posts by ISO weekday for (const post of posts) { const jsDay = new Date(post.publishedDate).getUTCDay(); const isoDay = toISOWeekday(jsDay); buckets.get(isoDay)!.push(post); } // Build metrics per day const byDayOfWeek: DayOfWeekMetrics[] = []; for (let isoDay = 1; isoDay <= 7; isoDay++) { const dayPosts = buckets.get(isoDay)!; const jsDay = isoDay === 7 ? 0 : isoDay; const dayName = DAY_NAMES[jsDay]; if (dayPosts.length === 0) { byDayOfWeek.push({ dayName, dayIndex: isoDay, postCount: 0, avgImpressions: 0, avgEngagementRate: 0, }); continue; } const totalImpressions = dayPosts.reduce((sum, p) => sum + p.metrics.impressions, 0); const totalEngagement = dayPosts.reduce((sum, p) => sum + p.metrics.engagementRate, 0); const bestPost = dayPosts.reduce((best, p) => p.metrics.impressions > best.metrics.impressions ? p : best ); byDayOfWeek.push({ dayName, dayIndex: isoDay, postCount: dayPosts.length, avgImpressions: Math.round(totalImpressions / dayPosts.length), avgEngagementRate: parseFloat((totalEngagement / dayPosts.length).toFixed(1)), bestPost, }); } // Find best days const daysWithPosts = byDayOfWeek.filter(d => d.postCount > 0); const bestDayImpressions = daysWithPosts.length > 0 ? daysWithPosts.reduce((best, d) => d.avgImpressions > best.avgImpressions ? d : best).dayName : "N/A"; const bestDayEngagement = daysWithPosts.length > 0 ? daysWithPosts.reduce((best, d) => d.avgEngagementRate > best.avgEngagementRate ? d : best).dayName : "N/A"; // Date range const sortedDates = posts.map(p => p.publishedDate).sort(); const dateRange = posts.length > 0 ? { from: sortedDates[0], to: sortedDates[sortedDates.length - 1] } : { from: "", to: "" }; return { generatedAt: new Date().toISOString(), postsAnalyzed: posts.length, dateRange, byDayOfWeek, bestDayImpressions, bestDayEngagement, }; }