A real-time world statistics simulation dashboard built with Next.js, displaying global events like births, deaths, social events, and physiological data updated every 100ms.
- Real-time Updates: Statistics refresh every 100ms with smooth transitions
- π¬ Animated Numbers: Smooth spring-physics animations for all counters (configurable)
- Two Types of Statistics:
- Cumulative Stats: Events that accumulate over your session (deaths, births, injuries, marriages, etc.)
- Snapshot Stats: Real-time distribution by continent (people at home, at work, in transit, unemployed)
- Session Timer: Track how long you've been viewing the statistics
- Responsive Design: Mobile-first approach with horizontal scroll for wide tables
- Type-Safe: Full TypeScript implementation with no
anytypes - Number Formatting: Smart formatting (1.5K, 2.3M) with tabular numbers for stability
- Color-Coded Categories: Visual distinction between death, birth, social, and physiological events
- Framework: Next.js 14+ with App Router
- Language: TypeScript
- Styling: Tailwind CSS
- Animation: Framer Motion for smooth number transitions
- State Management: useMemo for derived calculations
- Data Tables: TanStack Table
- Package Manager: pnpm
- Code Quality: ESLint + Prettier
- Node.js 18+
- npm or yarn
-
Clone the repository
git clone https://github.com/vavanesssa/weirdstats.git cd weirdstats -
Install dependencies
npm install
-
Run the development server
npm run dev
-
Open your browser Navigate to http://localhost:3000
npm run build
npm run startnpm run lint
npm run formatweirdstats/
βββ src/
β βββ app/ # Next.js App Router
β β βββ layout.tsx # Root layout with metadata
β β βββ page.tsx # Main dashboard page
β β βββ providers.tsx # TanStack Query provider
β β βββ globals.css # Global styles
β βββ components/
β β βββ stats/ # Statistics components
β β β βββ CumulativeStatsTable.tsx
β β β βββ SnapshotStatsTable.tsx
β β β βββ PopulationCounter.tsx
β β β βββ SessionTimer.tsx
β β β βββ StatRow.tsx
β β βββ ui/ # Base UI components
β β β βββ AnimatedNumber.tsx # π¬ Animated counter component
β β β βββ Table.tsx
β β β βββ Card.tsx
β β βββ layout/ # Layout components
β β βββ StatsLayout.tsx
β βββ hooks/ # Custom React hooks
β β βββ useStats.ts # Main stats aggregator
β β βββ useCumulativeStats.ts
β β βββ useSnapshotStats.ts
β β βββ useSessionTimer.ts
β βββ lib/ # Utility functions
β β βββ animationConfig.ts # ποΈ Animation settings & presets
β β βββ constants.ts # Rates, regions, pool volume
β β βββ calculations.ts # Stat calculation logic
β β βββ formatters.ts # Number formatting
β βββ types/
β βββ stats.ts # TypeScript interfaces
βββ package.json
βββ tsconfig.json
βββ tailwind.config.ts
βββ next.config.js
βββ README.md
Statistics are calculated using useMemo hooks that recompute when elapsed time changes:
- No external API calls - all data is derived from rates and elapsed time
- Smooth updates - timer ticks every 100ms, memoized calculations prevent unnecessary re-renders
- Type-safe - full TypeScript interfaces for all data structures
- Separation of Concerns: Business logic in
lib/, presentation incomponents/ - Composition over Configuration: Small, focused components that compose together
- Type Safety: Strict TypeScript with proper interfaces for all data structures
- Reusability: Generic UI components (
Card,Table,AnimatedNumber) used throughout
A reusable animated number component that smoothly transitions between values using spring physics.
- Component:
src/components/ui/AnimatedNumber.tsx - Configuration:
src/lib/animationConfig.ts
import { AnimatedNumber } from '@/components/ui/AnimatedNumber'
// Simple usage
<AnimatedNumber to={1500} />
// With formatting
<AnimatedNumber to={1500000} separator="," useCompactNotation={true} />| Property | Type | Default | Description |
|---|---|---|---|
to |
number |
required | The target number to animate to |
from |
number |
0 |
Initial number (only on mount) |
direction |
"up" | "down" |
"up" |
Direction of count |
delay |
number |
0 |
Delay before animation starts (seconds) |
duration |
number |
0.5 |
Animation duration factor |
className |
string |
"" |
CSS class for styling |
startWhen |
boolean |
true |
Control when animation starts |
separator |
string |
" " |
Thousands separator ( French, , US) |
useCompactNotation |
boolean |
true |
Use K/M suffixes |
compactDecimals |
number |
1 |
Decimals for K/M notation |
compactThresholdK |
number |
1000 |
Threshold for K suffix |
compactThresholdM |
number |
1000000 |
Threshold for M suffix |
damping |
number |
60 |
Spring damping (10-100) |
stiffness |
number |
100 |
Spring stiffness (50-500) |
mass |
number |
1 |
Spring mass (0.1-10) |
onStart |
() => void |
β | Callback when animation starts |
onEnd |
() => void |
β | Callback when animation ends |
Ready-to-use animation presets in src/lib/animationConfig.ts:
import { ANIMATION_PRESETS } from '@/lib/animationConfig'
// Balanced default
<AnimatedNumber to={100} {...ANIMATION_PRESETS.default} />
// Fast, snappy (good for rapid updates)
<AnimatedNumber to={100} {...ANIMATION_PRESETS.snappy} />
// Smooth, fluid (good for large changes)
<AnimatedNumber to={100} {...ANIMATION_PRESETS.smooth} />
// Bouncy, playful (good for celebrations)
<AnimatedNumber to={100} {...ANIMATION_PRESETS.bouncy} />
// Instant, no animation
<AnimatedNumber to={100} {...ANIMATION_PRESETS.instant} />Edit src/lib/animationConfig.ts to change defaults for all AnimatedNumber instances:
// βββ src/lib/animationConfig.ts βββββββββββββββββββββββββββββββ
export const DEFAULT_ANIMATION_CONFIG: AnimationConfig = {
// βββ Timing ββββββββββββββββββββββββββββββββββββββββββββββ
duration: 0.5, // Animation duration (seconds)
delay: 0, // Delay before start (seconds)
// βββ Spring Physics ββββββββββββββββββββββββββββββββββββββ
damping: 60, // Higher = less bouncy (10-100)
stiffness: 100, // Higher = faster snap (50-500)
mass: 1, // Higher = more inertia (0.1-10)
// βββ Formatting ββββββββββββββββββββββββββββββββββββββββββ
separator: ' ', // Thousands separator
useCompactNotation: true, // Show 1.5K, 1.5M
compactDecimals: 1, // Decimal places for K/M
compactThresholdK: 1000, // When to use K
compactThresholdM: 1000000, // When to use M
}The animation uses Framer Motion's spring physics:
| Parameter | Low Value | High Value |
|---|---|---|
| damping | More oscillation/bounce | Smooth, controlled |
| stiffness | Slow, fluid | Fast, snappy |
| mass | Light, responsive | Heavy, more inertia |
Recommended combinations:
- Real-time stats:
damping: 80, stiffness: 200(snappy preset) - Large counters:
damping: 40, stiffness: 60(smooth preset) - Gamification:
damping: 15, stiffness: 80(bouncy preset)
- All statistics have proper TypeScript interfaces
- No
anytypes allowed (enforced by ESLint) - Utility functions have JSDoc comments with examples
- Type inference used where appropriate to reduce boilerplate
These statistics accumulate over time since you loaded the page:
- Deaths: Broken down by cause (murder, suicide, tobacco, hunger, road accidents, other)
- Births: Total births worldwide
- Net Growth: Births minus deaths
- Injuries: Broken legs and arms
- Social Events: Marriages, divorces, abandonments, adoptions
- Physiological: Poop, pee, sneezes, water consumption (in liters)
- Pool Equivalents: Olympic pools filled with waste products
These statistics show the current state based on time of day in each region:
- At Home: People currently at home
- At Work: People currently at work (based on work hours 9am-6pm)
- In Transit: People commuting (7-9am, 6-8pm)
- Unemployed: Based on regional unemployment rates
- Session Timer: Tracks elapsed time in seconds
- Rate Application: Each event has a rate per second (e.g., 4.43 births/second)
- Cumulative Calculation:
event_count = rate Γ elapsed_seconds - Population Updates:
current_population = base_population + net_growth - Regional Distribution: Based on time zones and work/home patterns
Rates are derived from:
- WHO mortality data
- UN birth rate statistics
- International labor statistics
- Approximate physiological averages
The design follows a clean, minimal aesthetic:
-
Color Coding:
- Deaths: Red (red-50/red-700)
- Births: Green (green-50/green-700)
- Growth: Blue (blue-50/blue-600)
- Injuries: Orange (orange-50/orange-700)
- Social: Amber (amber-50/amber-700)
- Physiological: Purple (purple-50/purple-700)
- Pools: Cyan (cyan-50/cyan-800)
-
Typography:
- Tabular numbers for stable display
- Inter font family
- Responsive font sizes
Currently in French with structure ready for i18n:
- All labels could be extracted to locale files
- Number formatting supports different locales
- Date/time formatting is locale-aware
Contributions are welcome! Here's how:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow the existing code style (Prettier configuration)
- Write TypeScript with proper types (no
any) - Add JSDoc comments for utility functions
- Keep components small and focused
- Extract magic numbers to constants
- Test your changes thoroughly
MIT License - see LICENSE file for details
- Inspired by real-world statistics dashboards
- Built with modern React and Next.js patterns
- Powered by the TanStack ecosystem
Made with β€οΈ for understanding our world through data