cyrilsnare's picture
implement the global event heatmap in 8bit style
d02abfa verified
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EventScrape Analytics Hub</title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tsparticles/confetti@3.0.3/tsparticles.confetti.bundle.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/8bitcn-ui@latest/dist/8bitcn-ui.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/8bitcn-ui@latest/dist/8bitcn-ui.js"></script>
<style>
.world-map {
width: 100%;
height: 600px;
background: #000;
border: 4px solid #00ff00;
image-rendering: pixelated;
position: relative;
overflow: hidden;
}
.pixel-map {
image-rendering: pixelated;
background: #000;
}
.map-grid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(#00ff00 1px, transparent 1px),
linear-gradient(90deg, #00ff00 1px, transparent 1px);
background-size: 20px 20px;
opacity: 0.3;
pointer-events: none;
}
.country-pixel {
fill: #000;
stroke: #00ff00;
stroke-width: 2px;
image-rendering: pixelated;
transition: all 0.2s;
}
.country-pixel:hover {
stroke: #ffff00;
stroke-width: 3px;
filter: drop-shadow(0 0 4px #ffff00);
}
.heat-1 { fill: #004400; }
.heat-2 { fill: #008800; }
.heat-3 { fill: #00cc00; }
.heat-4 { fill: #00ff00; }
.heat-5 { fill: #88ff00; }
.heat-6 { fill: #ffff00; }
.heat-7 { fill: #ff8800; }
.heat-8 { fill: #ff0000; }
.map-legend {
position: absolute;
bottom: 10px;
right: 10px;
background: #000;
border: 3px solid #00ff00;
padding: 8px;
image-rendering: pixelated;
}
.legend-item {
display: flex;
align-items: center;
margin: 4px 0;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.legend-color {
width: 16px;
height: 16px;
margin-right: 8px;
border: 1px solid #00ff00;
image-rendering: pixelated;
}
.map-tooltip {
position: absolute;
background: #000;
border: 3px solid #ffff00;
padding: 8px;
font-family: 'Courier New', monospace;
font-size: 12px;
image-rendering: pixelated;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
z-index: 100;
}
.pulse-dot {
animation: pixel-pulse 1.5s infinite;
image-rendering: pixelated;
}
@keyframes pixel-pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.3); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
.pulse-ring {
animation: pulse 1s infinite;
image-rendering: pixelated;
}
@keyframes pulse {
0% { transform: scale(0.8); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.7; }
100% { transform: scale(0.8); opacity: 1; }
}
.ticker-item {
animation: ticker-scroll 20s linear infinite;
font-family: 'Courier New', monospace;
}
@keyframes ticker-scroll {
0% { transform: translateX(100%); }
100% { transform: translateX(-100%); }
}
.pixel-border {
border: 4px solid #00ff00;
border-image: repeating-linear-gradient(45deg, #00ff00, #00ff00 4px, #000 4px, #000 8px) 4;
image-rendering: pixelated;
}
.pixel-bg {
background: #000;
border: 2px solid #00ff00;
image-rendering: pixelated;
}
.pixel-text {
font-family: 'Courier New', monospace;
text-shadow: 2px 2px 0 #000;
letter-spacing: 1px;
}
.pixel-button {
background: #000;
border: 3px solid #00ff00;
font-family: 'Courier New', monospace;
text-shadow: 1px 1px 0 #000;
image-rendering: pixelated;
transition: all 0.1s;
}
.pixel-button:hover {
background: #00ff00;
color: #000;
transform: translateY(-2px);
box-shadow: 0 4px 0 #008800;
}
.pixel-button:active {
transform: translateY(0);
box-shadow: 0 2px 0 #008800;
}
.pixel-card {
background: #000;
border: 4px solid #00ff00;
box-shadow: 8px 8px 0 #008800;
image-rendering: pixelated;
}
.pixel-nav {
background: #000;
border-right: 4px solid #00ff00;
border-bottom: 4px solid #00ff00;
image-rendering: pixelated;
}
.pixel-checkbox {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #00ff00;
background: #000;
image-rendering: pixelated;
}
.pixel-checkbox:checked {
background: #00ff00;
box-shadow: inset 0 0 0 2px #000;
}
.pixel-badge {
font-family: 'Courier New', monospace;
border: 2px solid;
padding: 2px 8px;
font-size: 12px;
image-rendering: pixelated;
}
body {
font-family: 'Courier New', monospace;
background: #000 !important;
color: #00ff00 !important;
image-rendering: pixelated;
}
.dark body {
background: #000 !important;
color: #00ff00 !important;
}
</style>
</head>
<body class="bg-black text-green-400 transition-colors duration-300 pixel-text">
<!-- Theme Toggler -->
<div class="fixed top-4 right-4 z-50">
<button id="themeToggle" class="p-3 pixel-button">
<i data-feather="moon" class="hidden dark:block"></i>
<i data-feather="sun" class="dark:hidden"></i>
</button>
</div>
<!-- Main Layout -->
<div class="min-h-screen flex">
<!-- Sidebar -->
<aside class="w-64 pixel-nav p-6 fixed h-screen z-40">
<div class="flex items-center space-x-3 mb-8">
<div class="w-10 h-10 bg-green-500 pixel-border flex items-center justify-center">
<i data-feather="activity" class="text-black"></i>
</div>
<h1 class="text-xl font-bold pixel-text">EVENT_SCRAPE</h1>
</div>
<nav class="space-y-2">
<a href="#" class="flex items-center space-x-3 p-3 pixel-bg bg-green-500 text-black">
<i data-feather="home"></i>
<span>DASHBOARD</span>
</a>
<a href="#" class="flex items-center space-x-3 p-3 pixel-bg hover:bg-green-500 hover:text-black">
<i data-feather="compass"></i>
<span>SCRAPE_RUNS</span>
</a>
<a href="#" class="flex items-center space-x-3 p-3 pixel-bg hover:bg-green-500 hover:text-black">
<i data-feather="database"></i>
<span>DATA_MGMT</span>
</a>
<a href="#" class="flex items-center space-x-3 p-3 pixel-bg hover:bg-green-500 hover:text-black">
<i data-feather="bar-chart-2"></i>
<span>ANALYTICS</span>
</a>
<a href="#" class="flex items-center space-x-3 p-3 pixel-bg hover:bg-green-500 hover:text-black">
<i data-feather="settings"></i>
<span>SETTINGS</span>
</a>
</nav>
<!-- Map Filters -->
<div class="mt-8">
<h3 class="font-semibold mb-4 pixel-text">MAP_LAYERS</h3>
<div class="space-y-2">
<label class="flex items-center space-x-3">
<input type="checkbox" checked class="pixel-checkbox" data-layer="events">
<span class="pixel-text">EVENT_COUNT</span>
</label>
<label class="flex items-center space-x-3">
<input type="checkbox" class="pixel-checkbox" data-layer="music">
<span class="pixel-text">MUSIC_STYLES</span>
</label>
<label class="flex items-center space-x-3">
<input type="checkbox" class="pixel-checkbox" data-layer="capacity">
<span class="pixel-text">VENUE_CAPACITY</span>
</label>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 ml-64 p-6">
<!-- Header with Alerts Ticker -->
<header class="mb-8">
<div class="pixel-card p-4 mb-4 overflow-hidden">
<div class="flex items-center space-x-4">
<i data-feather="alert-circle" class="text-red-400"></i>
<div class="flex-1 overflow-hidden">
<div class="ticker-container whitespace-nowrap">
<div class="ticker-item inline-block px-4">
[ALERT] HIGH_LATENCY_NORTH_AMERICA •
</div>
<div class="ticker-item inline-block px-4">
[OK] PLUGIN_v2.3.1_DEPLOYED •
</div>
<div class="ticker-item inline-block px-4">
[SYNC] REAL_TIME_ACTIVE_ALL_REGIONS •
</div>
</div>
</div>
</div>
</div>
</header>
<!-- KPI Cards Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Events Scraped -->
<div class="glass-effect rounded-xl p-6 relative overflow-hidden">
<div class="absolute top-0 right-0 w-20 h-20 bg-blue-500/10 rounded-full -mr-4 -mt-4"></div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">Events Scraped</h3>
<i data-feather="calendar" class="text-blue-500"></i>
</div>
<div class="text-3xl font-bold mb-2" id="eventsCounter">2,847,392</div>
<div class="text-sm text-green-500 flex items-center">
<i data-feather="trending-up" class="w-4 h-4 mr-1"></i>
+12.4% this week
</div>
</div>
<!-- Plugins Built -->
<div class="glass-effect rounded-xl p-6 relative overflow-hidden">
<div class="absolute top-0 right-0 w-20 h-20 bg-purple-500/10 rounded-full -mr-4 -mt-4"></div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">Plugins Built</h3>
<i data-feather="package" class="text-purple-500"></i>
</div>
<div class="text-3xl font-bold mb-2" id="pluginsCounter">1,284</div>
<div class="text-sm text-green-500 flex items-center">
<i data-feather="trending-up" class="w-4 h-4 mr-1"></i>
+3 new today
</div>
</div>
<!-- Cities Covered -->
<div class="glass-effect rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">Cities Covered</h3>
<i data-feather="map-pin" class="text-green-500"></i>
</div>
<div class="text-3xl font-bold mb-2">842</div>
<div class="text-sm text-slate-600 dark:text-slate-400">Across 64 countries</div>
</div>
<!-- Active Scrapers -->
<div class="glass-effect rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">Active Scrapers</h3>
<i data-feather="cpu" class="text-orange-500"></i>
</div>
<div class="text-3xl font-bold mb-2">47</div>
<div class="text-sm text-slate-600 dark:text-slate-400">98.3% uptime</div>
</div>
</div>
<!-- Interactive World Map -->
<div class="pixel-card p-6 mb-8">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold pixel-text">GLOBAL_EVENT_HEAT_MAP</h2>
<div class="flex space-x-2">
<button class="pixel-button">
<i data-feather="download" class="w-4 h-4 mr-2"></i>
EXPORT
</button>
<button class="pixel-button">
<i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
REFRESH
</button>
</div>
</div>
<div id="worldMap" class="world-map">
<div class="map-grid"></div>
<svg id="pixelMap" class="pixel-map" width="100%" height="100%"></svg>
<div id="mapTooltip" class="map-tooltip"></div>
<div class="map-legend">
<div class="legend-item">
<div class="legend-color heat-1"></div>
<span>0-100K</span>
</div>
<div class="legend-item">
<div class="legend-color heat-3"></div>
<span>100K-500K</span>
</div>
<div class="legend-item">
<div class="legend-color heat-5"></div>
<span>500K-1M</span>
</div>
<div class="legend-item">
<div class="legend-color heat-7"></div>
<span>1M+</span>
</div>
</div>
</div>
</div>
<!-- Scrape Runs & Metrics -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Recent Scrape Runs -->
<div class="glass-effect rounded-xl p-6">
<h3 class="text-lg font-bold mb-4">Recent Scrape Runs</h3>
<div class="space-y-4">
<div class="flex items-center justify-between p-3 rounded-lg bg-slate-100/50 dark:bg-slate-800/50">
<div class="flex items-center space-x-3">
<div class="w-3 h-3 bg-green-500 rounded-full pulse-ring"></div>
<span>North America</span>
</div>
<div class="text-sm">Quality: 94%</div>
</div>
<div class="flex items-center justify-between p-3 rounded-lg bg-slate-100/50 dark:bg-slate-800/50">
<div class="flex items-center space-x-3">
<div class="w-3 h-3 bg-yellow-500 rounded-full"></div>
<span>Europe</span>
</div>
<div class="text-sm">Quality: 87%</div>
</div>
<div class="flex items-center justify-between p-3 rounded-lg bg-slate-100/50 dark:bg-slate-800/50">
<div class="flex items-center space-x-3">
<div class="w-3 h-3 bg-red-500 rounded-full"></div>
<span>Asia Pacific</span>
</div>
<div class="text-sm">Quality: 72%</div>
</div>
</div>
</div>
<!-- Event Type Distribution -->
<div class="glass-effect rounded-xl p-6">
<h3 class="text-lg font-bold mb-4">Events by Type</h3>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span>Music Concerts</span>
<span class="font-semibold">784,239</span>
</div>
<div class="flex items-center justify-between">
<span>Sports Events</span>
<span class="font-semibold">492,184</span>
</div>
<div class="flex items-center justify-between">
<span>Festivals</span>
<span class="font-semibold">318,752</span>
</div>
<div class="flex items-center justify-between">
<span>Theater & Arts</span>
<span class="font-semibold">275,491</span>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="mt-12 pt-8 border-t border-slate-200 dark:border-slate-700">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="text-sm text-slate-600 dark:text-slate-400">
© 2024 EventScrape Analytics Hub. All rights reserved.
</div>
<div class="flex space-x-6 mt-4 md:mt-0">
<span class="text-sm">Last sync: 2 minutes ago</span>
<span class="text-sm">Data freshness: 99.8%</span>
<span class="text-sm">System status: Operational</span>
</div>
</div>
</footer>
</main>
</div>
<script>
// Theme Toggler
document.getElementById('themeToggle').addEventListener('click', function() {
document.documentElement.classList.toggle('dark');
feather.replace();
});
// WebSocket Simulation for Real-time Updates
function simulateWebSocketUpdates() {
// Events counter animation
let events = 2847392;
setInterval(() => {
events += Math.floor(Math.random() * 100);
document.getElementById('eventsCounter').textContent = events.toLocaleString();
}, 2000);
// Plugins counter animation
let plugins = 1284;
setInterval(() => {
plugins += Math.random() > 0.7 ? 1 : 0;
document.getElementById('pluginsCounter').textContent = plugins.toLocaleString();
}, 5000);
}
// 8-bit World Map with D3.js
function initWorldMap() {
const svg = d3.select("#pixelMap");
const tooltip = d3.select("#mapTooltip");
// Simplified projection for pixelated look
const projection = d3.geoMercator()
.scale(120)
.translate([400, 300]);
const path = d3.geoPath().projection(projection);
// Enhanced sample data for heat map
const sampleData = {
"USA": { events: 842000, music: "Rock", capacity: 2850000, cities: 245 },
"GBR": { events: 467000, music: "Pop", capacity: 1280000, cities: 89 },
"DEU": { events: 328000, music: "Electronic", capacity: 920000, cities: 76 },
"FRA": { events: 291000, music: "Hip Hop", capacity: 780000, cities: 68 },
"JPN": { events: 218000, music: "J-Pop", capacity: 640000, cities: 52 },
"CAN": { events: 187000, music: "Indie", capacity: 520000, cities: 45 },
"AUS": { events: 156000, music: "Rock", capacity: 410000, cities: 38 },
"BRA": { events: 134000, music: "Samba", capacity: 380000, cities: 41 },
"ITA": { events: 123000, music: "Opera", capacity: 320000, cities: 35 },
"ESP": { events: 118000, music: "Flamenco", capacity: 290000, cities: 32 }
};
// Get heat level based on event count
function getHeatLevel(events) {
if (events > 1000000) return 8;
if (events > 800000) return 7;
if (events > 600000) return 6;
if (events > 400000) return 5;
if (events > 200000) return 4;
if (events > 100000) return 3;
if (events > 50000) return 2;
return 1;
}
// Draw simplified pixelated map
const simplifiedWorld = {
type: "FeatureCollection",
features: [
// North America
{ type: "Feature", geometry: { type: "Polygon", coordinates: [[[-130, 25], [-130, 50], [-65, 50], [-65, 25], [-130, 25]]] }, properties: { name: "North America", id: "USA" }},
// Europe
{ type: "Feature", geometry: { type: "Polygon", coordinates: [[[-10, 35], [-10, 60], [40, 60], [40, 35], [-10, 35]]] }, properties: { name: "Europe", id: "EUR" }},
// Asia
{ type: "Feature", geometry: { type: "Polygon", coordinates: [[[60, 10], [60, 60], [140, 60], [140, 10], [60, 10]]] }, properties: { name: "Asia", id: "ASIA" }},
// South America
{ type: "Feature", geometry: { type: "Polygon", coordinates: [[[-80, -55], [-80, 15], [-35, 15], [-35, -55], [-80, -55]]] }, properties: { name: "South America", id: "SAM" }},
// Africa
{ type: "Feature", geometry: { type: "Polygon", coordinates: [[[-20, -35], [-20, 35], [50, 35], [50, -35], [-20, -35]]] }, properties: { name: "Africa", id: "AFR" }},
// Australia
{ type: "Feature", geometry: { type: "Polygon", coordinates: [[[110, -45], [110, -10], [155, -10], [155, -45], [110, -45]]] }, properties: { name: "Australia", id: "AUS" }}
]
};
// Draw pixelated countries
svg.selectAll("path")
.data(simplifiedWorld.features)
.enter()
.append("path")
.attr("d", path)
.attr("class", "country-pixel")
.attr("fill", function(d) {
const countryData = sampleData[d.properties.id];
if (countryData) {
const heatLevel = getHeatLevel(countryData.events);
return `var(--heat-${heatLevel})`;
}
return "#000";
})
.on("mouseover", function(event, d) {
const countryData = sampleData[d.properties.id];
if (countryData) {
tooltip
.style("opacity", 1)
.html(`
<div class="font-bold">${d.properties.name}</div>
<div>Events: ${countryData.events.toLocaleString()}</div>
<div>Top Genre: ${countryData.music}</div>
<div>Cities: ${countryData.cities}</div>
<div>Capacity: ${countryData.capacity.toLocaleString()}</div>
`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px");
}
d3.select(this)
.style("filter", "drop-shadow(0 0 6px #ffff00)");
})
.on("mousemove", function(event) {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px");
})
.on("mouseout", function() {
tooltip.style("opacity", 0);
d3.select(this)
.style("filter", "none");
});
// Add pulse dots for major cities
const majorCities = [
{ name: "New York", coords: [-74, 40.7], events: 284000 },
{ name: "London", coords: [-0.1, 51.5], events: 198000 },
{ name: "Tokyo", coords: [139.7, 35.7], events: 167000 },
{ name: "Berlin", coords: [13.4, 52.5], events: 142000 },
{ name: "Paris", coords: [2.3, 48.9], events: 128000 },
{ name: "Sydney", coords: [151.2, -33.9], events: 98000 },
{ name: "São Paulo", coords: [-46.6, -23.6], events: 87000 }
];
svg.selectAll("circle")
.data(majorCities)
.enter()
.append("circle")
.attr("cx", d => projection(d.coords)[0])
.attr("cy", d => projection(d.coords)[1])
.attr("r", 4)
.attr("class", "pulse-dot")
.attr("fill", "#ffff00")
.attr("stroke", "#000")
.attr("stroke-width", 1);
}
</body>
</html>