A Spotify Wrapped-style dashboard that analyzes YouTube watch history from Google Takeout and presents personalized insights through an immersive, data-driven interface.
- File:
watch-history.htmlfrom Google Takeout - Format: HTML with Material Design Lite structure
- Encoding: UTF-8
- Size: Variable (can be 1MB+)
<div class="content-cell mdl-cell mdl-cell--6-col mdl-typography--body-1">
Watched <a href="https://www.youtube.com/watch?v={VIDEO_ID}">{VIDEO_TITLE}</a><br>
<a href="https://www.youtube.com/channel/{CHANNEL_ID}">{CHANNEL_NAME}</a><br>
{TIMESTAMP}<br>
</div>- Action Type: "Watched" or "Viewed" (prefix before video link)
- Video ID: From URL parameter
watch?v= - Video Title: Link text content
- Channel ID: From channel URL
- Channel Name: Channel link text content
- Timestamp: Format
DD MMM YYYY, HH:MM:SS GMT
- Viewed posts (not videos) - should be filtered out
- Missing channel information
- Malformed timestamps
- Special characters in titles (HTML entities)
- Very long video titles (truncation)
Input: Raw HTML string Output: Array of watch events
interface WatchEvent {
action: 'watched' | 'viewed';
videoId: string;
videoUrl: string;
title: string;
channelId: string;
channelName: string;
timestamp: Date;
year: number;
month: number; // 0-11
dayOfWeek: number; // 0-6 (Sun-Sat)
dayOfMonth: number; // 1-31
hour: number; // 0-23
}Processing Steps:
- Parse HTML and find all
.content-celldivs - For each cell, extract action type using regex:
/(Watched|Viewed)\s+<a/ - Extract video URL from first anchor tag
- Parse video ID from URL query parameter
- Extract video title from anchor text
- Extract channel info from second anchor tag
- Parse timestamp string to Date object
- Calculate derived time fields (year, month, hour, etc.)
- Filter out non-video content (posts, community content)
Criteria:
- Only include year 2025 (configurable)
- Only include "Watched" actions (exclude "Viewed")
- Remove entries with missing critical fields (videoId, timestamp)
- Deduplicate if necessary (keep first occurrence)
1. Volume Metrics
{
totalVideos: number;
uniqueChannels: number;
totalHoursEstimated: number; // totalVideos * 10min / 60
averageVideosPerDay: number;
dateRange: { start: Date, end: Date };
}2. Channel Analytics
interface ChannelStats {
channelName: string;
channelId: string;
videoCount: number;
percentage: number;
firstWatched: Date;
lastWatched: Date;
}
// Output: Array sorted by videoCount DESC, top 103. Temporal Patterns
{
byHour: number[]; // Array[24] - count per hour
byDayOfWeek: number[]; // Array[7] - count per day (Sun-Sat)
byMonth: number[]; // Array[12] - count per month
byDate: Map<string, number>; // ISO date string -> count
peakHour: number;
peakDay: string; // "Monday", "Tuesday", etc.
peakMonth: string; // "January", "February", etc.
weekdayCount: number;
weekendCount: number;
}4. Binge Detection
interface BingeSession {
startTime: Date;
endTime: Date;
videoCount: number;
durationMinutes: number;
videos: Array<{title: string, channel: string}>;
dominantChannel?: string;
}
// Algorithm:
// 1. Sort events by timestamp
// 2. Group consecutive videos within 2-hour windows
// 3. Sessions with 5+ videos are "binges"
// 4. Sessions with 10+ videos are "rabbit holes"5. Content Categorization
interface CategoryStats {
music: number;
gaming: number;
education: number;
entertainment: number;
tech: number;
podcasts: number;
news: number;
sports: number;
other: number;
}
// Classification Rules (keywords in title/channel, case-insensitive):
// - Music: "official video", "lyrics", "audio", "song", "music"
// - Gaming: "gameplay", "let's play", "gaming", "walkthrough", "playthrough"
// - Education: "tutorial", "how to", "course", "explained", "lesson", "guide"
// - Entertainment: "vlog", "comedy", "challenge", "prank", "reaction"
// - Tech: "review", "unboxing", "tech", "smartphone", "laptop", "gadget"
// - Podcasts: "podcast", "interview", "talk show"
// - News: "news", "breaking", "headlines"
// - Sports: "match", "game", "highlights", "cricket", "football", "basketball"6. Rewatch Analysis
interface RewatchStats {
mostRewatchedVideo: {
videoId: string;
title: string;
channel: string;
count: number;
};
totalRewatches: number;
rewatchRate: number; // percentage
comfortChannels: string[]; // Channels with most rewatches
}7. Fun Facts
interface FunFacts {
lateNightCount: number; // 11pm-5am
earlyBirdCount: number; // 5am-8am
firstVideoOf2025: WatchEvent;
lastVideoOf2025: WatchEvent;
video1000: WatchEvent | null;
longestTitle: string;
mostCommonWord: string; // Excluding common words
procrastinationScore: number; // Weekday 9am-5pm watches
longestStreak: number; // Consecutive days with watches
}YouTubeWrapped (main component)
├── FileUploader
├── LoadingScreen
└── Dashboard
├── Navigation (tabs)
├── HeroSection
├── ChannelsSection
├── PatternsSection
├── CategoriesSection
├── BingesSection
└── FunFactsSection
Layout: Full-screen centered grid
Content:
{
mainMetric: "X,XXX Videos Watched",
subMetrics: [
"XXX Hours Estimated",
"XX Videos/Day Average",
"Top Channel: {name}"
],
dateRange: "January 1 - December 31, 2025"
}Styling:
- Background: Gradient (purple/pink to red)
- Main number: 8xl font, bold
- Sub metrics: 2xl font
- Animated counter effect on load
Layout: Vertical list with horizontal bars
Content:
channels.slice(0, 10).map(channel => ({
rank: number,
name: string,
count: number,
percentage: string,
barWidth: string // percentage
}))Features:
- #1 channel has special gold styling
- Top 3 have podium colors
- Hover shows additional stats
- Animated bar growth on scroll-in
Sub-section A: Time Heatmap
- Grid: 7 rows (days) × 24 columns (hours)
- Color scale: light to intense red based on watch count
- Show peak time with highlight
- Tooltip on hover shows exact count
Sub-section B: Charts
- Hour distribution: Horizontal bar chart
- Day of week: Vertical bar chart
- Monthly trend: Line chart with area fill
- Weekend vs Weekday: Donut chart
Insights Box:
- Badge: "Night Owl" (peak 9pm-3am) or "Early Bird" (5am-9am)
- Text: "Most active on {day}"
- Text: "Binge month: {month}"
Layout: Two-column layout
Left Column: Pie/Donut chart
- Show all categories with percentages
- Highlight top 3 with labels
- Interactive hover
Right Column:
- "Your YouTube Personality" card based on top category
- Category descriptions
- Monthly trend line for top 3 categories
Personality Mapping:
- Music dominant → "The Audiophile"
- Gaming dominant → "The Gamer"
- Education dominant → "The Lifelong Learner"
- Entertainment → "The Fun Seeker"
- Mixed → "The Explorer"
Sub-section A: Top Binge Sessions
sessions.slice(0, 5).map(session => ({
date: formatted date,
videoCount: number,
duration: "X hours Y minutes",
topic: dominant channel or category,
timeline: visual representation
}))Sub-section B: Rewatch Stats
- Card: Most rewatched video (with count)
- List: Comfort content channels
- Metric: Rewatch rate percentage
- Badge: "Rabbit Hole Count" (10+ video sessions)
Layout: Grid of fact cards (2-3 columns)
Content (prioritized list):
- Late night watching count
- First & last video of 2025
- Most common title word
- Procrastination score
- Video #1000 milestone
- Longest title
- Longest watching streak
- Unusual patterns
Styling: Each card has icon, stat, and description
- Framework: React 18+
- Build: Single .jsx file (for simplicity)
- Charts: Recharts library
- Styling: Tailwind CSS
- Icons: lucide-react
- HTML Parsing: Browser DOMParser API
const [uploadedFile, setUploadedFile] = useState(null);
const [rawData, setRawData] = useState([]);
const [analytics, setAnalytics] = useState(null);
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('hero');
const [yearFilter, setYearFilter] = useState(2025);- Parse HTML in Web Worker (if available)
- Virtualize long lists (channel rankings)
- Lazy load chart components
- Memoize expensive calculations
- Debounce filter changes
- Mobile: < 640px (single column, simplified charts)
- Tablet: 640px - 1024px (two columns)
- Desktop: > 1024px (full layout)
--youtube-red: #FF0000;
--youtube-dark: #282828;
--wrapped-purple: #8b5cf6;
--wrapped-pink: #ec4899;
--bg-light: #fafafa;
--bg-dark: #0f0f0f;
--text-primary: #0f0f0f;
--text-secondary: #606060;
--accent-gold: #ffd700;
--accent-silver: #c0c0c0;
--accent-bronze: #cd7f32;- Invalid file type → Show error message
- File too large (>50MB) → Warn and attempt anyway
- Parse failure → Show sample of problematic content
- No 2025 data → Suggest year filter change
- Empty file → Clear error message
- Malformed entries → Skip and log count
- No data for category → Show "No data" placeholder
- Division by zero → Handle gracefully
- Overflow → Truncate with ellipsis
- ✅ Parse any valid Google Takeout watch-history.html
- ✅ Display accurate statistics for 2025
- ✅ All 6 sections render correctly
- ✅ Charts are interactive and visually appealing
- ✅ Mobile responsive design works
- ✅ Handles edge cases gracefully
- ✅ Fun, shareable insights
- ✅ Loads and processes within 5 seconds for typical file
File: youtube-wrapped.jsx
Usage:
# With create-react-app
npm create vite@latest youtube-wrapped -- --template react
cd youtube-wrapped
# Copy youtube-wrapped.jsx to src/
npm install recharts lucide-react
npm run devStandalone HTML Version (bonus): Single HTML file with inline React/Recharts from CDN for easy sharing.
- Start with HTML parser - test with sample data first
- Build analytics engine - verify calculations manually
- Create static UI with mock data
- Connect data pipeline to UI
- Add interactivity and polish
- Test with real watch-history.html file
- Optimize performance if needed
watch-history.html
→ parseHTML()
→ Array<WatchEvent>
→ filterByYear(2025)
→ calculateAnalytics()
→ {
volume: {...},
channels: [...],
patterns: {...},
categories: {...},
binges: [...],
rewatches: {...},
funFacts: {...}
}
→ render Dashboard sections