Practice and reinforce the concepts from Lesson 10
In this activity, you'll:
Total time: 60-75 minutes
Access the template here: Real-time Data Template
git clone
and navigate to the template folderindex.html
in your browser to start!What you'll do: Pick your live data scenario Time: 5 minutes
Choose one of these exciting real-time projects:
Live tracking of Pokemon stats and team composition
Real-time character database with live updates
Live tracking of ISS location and space data
Real-time sports data and game updates
Real-time crypto prices and market data
What you'll do: Build an intelligent data polling manager Time: 15 minutes
class UniversalPollingManager {
constructor() {
this.activePollers = new Map();
this.pollingStrategies = {
'pokemon': { defaultInterval: 30000, adaptiveRange: [10000, 60000] },
'starwars': { defaultInterval: 120000, adaptiveRange: [60000, 300000] },
'iss': { defaultInterval: 10000, adaptiveRange: [5000, 30000] },
'sports': { defaultInterval: 5000, adaptiveRange: [2000, 15000] },
'crypto': { defaultInterval: 15000, adaptiveRange: [5000, 60000] }
};
this.globalState = {
isVisible: true,
isOnline: navigator.onLine,
batteryLevel: 1,
performanceMode: 'normal'
};
this.setupGlobalHandlers();
}
createPoller(id, fetchFunction, options = {}) {
const strategy = this.pollingStrategies[options.dataType] || this.pollingStrategies['pokemon'];
const poller = {
id: id,
fetchFunction: fetchFunction,
dataType: options.dataType || 'generic',
interval: options.interval || strategy.defaultInterval,
onData: options.onData || (() => {}),
onError: options.onError || console.error,
priority: options.priority || 'medium',
// State tracking
isActive: false,
timeoutId: null,
lastUpdate: null,
lastDataHash: null,
errorCount: 0,
successCount: 0,
// Adaptive behavior
adaptiveInterval: options.interval || strategy.defaultInterval,
adaptiveRange: strategy.adaptiveRange,
changeDetection: options.changeDetection !== false,
// Performance tracking
averageResponseTime: 0,
lastResponseTime: 0
};
this.activePollers.set(id, poller);
console.log(`📡 Created ${poller.dataType} poller: ${id}`);
return poller;
}
startPolling(id) {
const poller = this.activePollers.get(id);
if (!poller) {
console.error(`Poller ${id} not found`);
return;
}
if (poller.isActive) {
console.log(`Poller ${id} already active`);
return;
}
poller.isActive = true;
console.log(`▶️ Started ${poller.dataType} polling: ${id}`);
// Initial fetch
this.executePoll(poller);
}
stopPolling(id) {
const poller = this.activePollers.get(id);
if (!poller) return;
poller.isActive = false;
if (poller.timeoutId) {
clearTimeout(poller.timeoutId);
poller.timeoutId = null;
}
console.log(`⏸️ Stopped ${poller.dataType} polling: ${id}`);
}
async executePoll(poller) {
if (!poller.isActive) return;
// Skip polling based on global state
if (!this.shouldPoll(poller)) {
this.scheduleNextPoll(poller, this.getSkipDelay(poller));
return;
}
const startTime = Date.now();
try {
console.log(`🔄 Polling ${poller.dataType}: ${poller.id}`);
this.updatePollingIndicator(poller.id, 'fetching');
const data = await poller.fetchFunction();
const responseTime = Date.now() - startTime;
// Update performance metrics
this.updatePerformanceMetrics(poller, responseTime, true);
// Check if data has changed
const hasChanged = this.detectDataChange(poller, data);
if (hasChanged || !poller.changeDetection) {
poller.lastUpdate = Date.now();
poller.onData(data, {
hasChanged,
responseTime,
pollCount: poller.successCount
});
}
// Adapt polling frequency based on change patterns
this.adaptPollingFrequency(poller, hasChanged);
this.updatePollingIndicator(poller.id, 'success');
} catch (error) {
console.error(`❌ Poll failed for ${poller.dataType} ${poller.id}:`, error);
this.updatePerformanceMetrics(poller, Date.now() - startTime, false);
poller.onError(error);
// Exponential backoff for errors
this.handlePollingError(poller);
this.updatePollingIndicator(poller.id, 'error');
}
this.scheduleNextPoll(poller);
}
shouldPoll(poller) {
// Don't poll when tab is hidden (except high priority)
if (!this.globalState.isVisible && poller.priority !== 'high') {
return false;
}
// Don't poll when offline
if (!this.globalState.isOnline) {
return false;
}
// Reduce polling on low battery
if (this.globalState.batteryLevel < 0.2 && poller.priority === 'low') {
return false;
}
return true;
}
detectDataChange(poller, newData) {
if (!poller.changeDetection) return true;
const newHash = this.hashData(newData);
const hasChanged = poller.lastDataHash !== newHash;
poller.lastDataHash = newHash;
return hasChanged;
}
hashData(data) {
return JSON.stringify(data).split('').reduce((a, b) => {
a = ((a << 5) - a) + b.charCodeAt(0);
return a & a;
}, 0);
}
adaptPollingFrequency(poller, hasChanged) {
const [minInterval, maxInterval] = poller.adaptiveRange;
if (hasChanged) {
// Data is changing, poll more frequently
poller.adaptiveInterval = Math.max(
poller.adaptiveInterval * 0.8,
minInterval
);
} else {
// Data is stable, poll less frequently
poller.adaptiveInterval = Math.min(
poller.adaptiveInterval * 1.1,
maxInterval
);
}
console.log(`📊 Adapted ${poller.id} interval to ${poller.adaptiveInterval}ms`);
}
updatePerformanceMetrics(poller, responseTime, success) {
if (success) {
poller.successCount++;
poller.lastResponseTime = responseTime;
poller.averageResponseTime =
(poller.averageResponseTime * (poller.successCount - 1) + responseTime) / poller.successCount;
poller.errorCount = 0; // Reset error count on success
} else {
poller.errorCount++;
}
}
handlePollingError(poller) {
// Exponential backoff for errors
const backoffMultiplier = Math.min(Math.pow(2, poller.errorCount), 8);
poller.adaptiveInterval = poller.interval * backoffMultiplier;
// Stop polling after too many consecutive errors
if (poller.errorCount >= 5) {
console.error(`🛑 Stopping ${poller.id} due to repeated failures`);
this.stopPolling(poller.id);
}
}
scheduleNextPoll(poller, customDelay = null) {
if (!poller.isActive) return;
const delay = customDelay || this.calculateDelay(poller);
poller.timeoutId = setTimeout(() => {
this.executePoll(poller);
}, delay);
console.log(`⏰ Next poll for ${poller.id} in ${delay}ms`);
}
calculateDelay(poller) {
let delay = poller.adaptiveInterval;
// Adjust based on global state
if (!this.globalState.isVisible) {
delay *= 3; // Poll 3x less when hidden
}
if (this.globalState.batteryLevel < 0.3) {
delay *= 2; // Poll 2x less on low battery
}
if (this.globalState.performanceMode === 'eco') {
delay *= 2;
} else if (this.globalState.performanceMode === 'performance') {
delay *= 0.5;
}
return Math.max(delay, 1000); // Minimum 1 second
}
getSkipDelay(poller) {
// Longer delay when skipping polls
return Math.max(poller.adaptiveInterval * 2, 10000);
}
updatePollingIndicator(pollerId, status) {
const indicator = document.getElementById(`polling-${pollerId}`);
if (indicator) {
const statusIcons = {
'fetching': '🔄',
'success': '✅',
'error': '❌',
'skipped': '⏭️'
};
indicator.textContent = `${statusIcons[status]} ${new Date().toLocaleTimeString()}`;
indicator.className = `polling-indicator ${status}`;
}
}
setupGlobalHandlers() {
// Page visibility handling
document.addEventListener('visibilitychange', () => {
this.globalState.isVisible = !document.hidden;
console.log(`👁️ Page visibility: ${this.globalState.isVisible ? 'visible' : 'hidden'}`);
if (this.globalState.isVisible) {
this.resumeAllPolling();
}
});
// Online/offline handling
window.addEventListener('online', () => {
this.globalState.isOnline = true;
console.log('🌐 Back online - resuming polling');
this.resumeAllPolling();
});
window.addEventListener('offline', () => {
this.globalState.isOnline = false;
console.log('📵 Gone offline - pausing polling');
});
// Battery handling (if available)
this.setupBatteryHandling();
}
async setupBatteryHandling() {
try {
if ('getBattery' in navigator) {
const battery = await navigator.getBattery();
this.globalState.batteryLevel = battery.level;
battery.addEventListener('levelchange', () => {
this.globalState.batteryLevel = battery.level;
console.log(`🔋 Battery level: ${Math.round(battery.level * 100)}%`);
});
}
} catch (error) {
console.log('Battery API not available');
}
}
resumeAllPolling() {
this.activePollers.forEach((poller) => {
if (poller.isActive) {
console.log(`🔄 Resuming ${poller.id}`);
this.executePoll(poller);
}
});
}
getPollingStats() {
const stats = {
totalPollers: this.activePollers.size,
activePollers: 0,
pollerDetails: []
};
this.activePollers.forEach((poller) => {
if (poller.isActive) stats.activePollers++;
stats.pollerDetails.push({
id: poller.id,
dataType: poller.dataType,
active: poller.isActive,
interval: Math.round(poller.adaptiveInterval),
successCount: poller.successCount,
errorCount: poller.errorCount,
avgResponseTime: Math.round(poller.averageResponseTime),
lastUpdate: poller.lastUpdate ? new Date(poller.lastUpdate).toLocaleTimeString() : 'Never'
});
});
return stats;
}
setPerformanceMode(mode) {
this.globalState.performanceMode = mode;
console.log(`⚡ Performance mode: ${mode}`);
// Adjust all active pollers
this.activePollers.forEach((poller) => {
if (poller.isActive) {
// Force recalculation of timing
clearTimeout(poller.timeoutId);
this.scheduleNextPoll(poller);
}
});
}
}
// Global polling manager instance
const pollingManager = new UniversalPollingManager();
Your task:
pollingManager.getPollingStats()
What you'll do: Create real-time fetchers for your chosen API Time: 20 minutes
async function fetchISSPosition() {
try {
const response = await fetch('http://api.open-notify.org/iss-now.json');
if (!response.ok) throw new Error('ISS API error');
const data = await response.json();
return {
timestamp: data.timestamp,
latitude: parseFloat(data.iss_position.latitude),
longitude: parseFloat(data.iss_position.longitude),
altitude: 408, // ISS average altitude in km
speed: 27600 // ISS average speed in km/h
};
} catch (error) {
console.error('Failed to fetch ISS position:', error);
throw error;
}
}
async function fetchISSAstronauts() {
try {
const response = await fetch('http://api.open-notify.org/astros.json');
if (!response.ok) throw new Error('Astronauts API error');
const data = await response.json();
return {
total: data.number,
astronauts: data.people.filter(person => person.craft === 'ISS'),
lastUpdated: Date.now()
};
} catch (error) {
console.error('Failed to fetch astronaut data:', error);
throw error;
}
}
function setupISSTracking() {
// Create ISS position poller
pollingManager.createPoller('iss-position', fetchISSPosition, {
dataType: 'iss',
interval: 10000, // 10 seconds
priority: 'high',
changeDetection: true,
onData: (data, meta) => {
updateISSMap(data);
updateISSStats(data, meta);
},
onError: (error) => {
showISSError(error);
}
});
// Create astronaut data poller
pollingManager.createPoller('iss-astronauts', fetchISSAstronauts, {
dataType: 'iss',
interval: 300000, // 5 minutes
priority: 'medium',
onData: (data) => {
updateAstronautList(data);
}
});
// Start both pollers
pollingManager.startPolling('iss-position');
pollingManager.startPolling('iss-astronauts');
}
function updateISSMap(positionData) {
document.getElementById('iss-lat').textContent = positionData.latitude.toFixed(4);
document.getElementById('iss-lng').textContent = positionData.longitude.toFixed(4);
document.getElementById('iss-speed').textContent = positionData.speed;
// Update map marker if you have a map integration
if (window.issMap) {
window.issMap.updatePosition(positionData.latitude, positionData.longitude);
}
}
async function fetchCryptoPrices(symbols = ['bitcoin', 'ethereum', 'cardano']) {
try {
const symbolsParam = symbols.join(',');
const response = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${symbolsParam}&vs_currencies=usd&include_24hr_change=true`
);
if (!response.ok) throw new Error('CoinGecko API error');
const data = await response.json();
return Object.keys(data).map(symbol => ({
symbol: symbol,
price: data[symbol].usd,
change24h: data[symbol].usd_24h_change,
timestamp: Date.now()
}));
} catch (error) {
console.error('Failed to fetch crypto prices:', error);
throw error;
}
}
function setupCryptoTracking() {
pollingManager.createPoller('crypto-prices', () => fetchCryptoPrices(), {
dataType: 'crypto',
interval: 15000, // 15 seconds
priority: 'high',
changeDetection: true,
onData: (data, meta) => {
updateCryptoPrices(data);
if (meta.hasChanged) {
checkPriceAlerts(data);
}
},
onError: (error) => {
showCryptoError(error);
}
});
pollingManager.startPolling('crypto-prices');
}
function updateCryptoPrices(cryptoData) {
cryptoData.forEach(crypto => {
const container = document.getElementById(`crypto-${crypto.symbol}`);
if (container) {
const changeClass = crypto.change24h >= 0 ? 'positive' : 'negative';
const changeIcon = crypto.change24h >= 0 ? '🟢' : '🔴';
container.innerHTML = `
<div class="crypto-card">
<h3>${crypto.symbol.toUpperCase()}</h3>
<div class="price">${crypto.price.toFixed(2)}</div>
<div class="change ${changeClass}">
${changeIcon} ${crypto.change24h.toFixed(2)}%
</div>
</div>
`;
}
});
}
async function fetchPokemonTeamStats(pokemonNames) {
try {
const pokemonPromises = pokemonNames.map(async name => {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`);
if (!response.ok) throw new Error(`Pokemon ${name} not found`);
const data = await response.json();
return {
name: data.name,
id: data.id,
stats: data.stats.map(s => s.base_stat),
types: data.types.map(t => t.type.name),
sprite: data.sprites.front_default
};
});
const teamData = await Promise.all(pokemonPromises);
return {
team: teamData,
teamSize: teamData.length,
averageStats: calculateAverageStats(teamData),
timestamp: Date.now()
};
} catch (error) {
console.error('Failed to fetch Pokemon team:', error);
throw error;
}
}
function setupPokemonTeamTracking() {
const currentTeam = ['pikachu', 'charizard', 'blastoise']; // User's current team
pollingManager.createPoller('pokemon-team', () => fetchPokemonTeamStats(currentTeam), {
dataType: 'pokemon',
interval: 30000, // 30 seconds
priority: 'medium',
changeDetection: false, // Pokemon stats don't change often
onData: (data) => {
updateTeamDisplay(data);
updateTeamAnalysis(data);
},
onError: (error) => {
showPokemonError(error);
}
});
pollingManager.startPolling('pokemon-team');
}
Your task:
What you'll do: Add interactive dashboard controls Time: 15 minutes
class LiveDashboard {
constructor() {
this.notifications = [];
this.alerts = new Map();
this.dashboardState = {
autoRefresh: true,
soundEnabled: true,
compactMode: false
};
this.setupDashboardControls();
this.initializeNotificationSystem();
}
setupDashboardControls() {
// Auto-refresh toggle
document.getElementById('autoRefreshToggle')?.addEventListener('change', (e) => {
this.dashboardState.autoRefresh = e.target.checked;
if (this.dashboardState.autoRefresh) {
pollingManager.resumeAllPolling();
this.showNotification('Auto-refresh enabled', 'success');
} else {
pollingManager.activePollers.forEach((poller, id) => {
pollingManager.stopPolling(id);
});
this.showNotification('Auto-refresh disabled', 'warning');
}
});
// Performance mode controls
document.getElementById('ecoMode')?.addEventListener('click', () => {
pollingManager.setPerformanceMode('eco');
this.showNotification('Eco mode enabled - reduced polling frequency', 'info');
});
document.getElementById('normalMode')?.addEventListener('click', () => {
pollingManager.setPerformanceMode('normal');
this.showNotification('Normal mode enabled', 'info');
});
document.getElementById('performanceMode')?.addEventListener('click', () => {
pollingManager.setPerformanceMode('performance');
this.showNotification('Performance mode enabled - increased polling frequency', 'info');
});
// Manual refresh button
document.getElementById('manualRefresh')?.addEventListener('click', () => {
this.manualRefreshAll();
});
// Pause all button
document.getElementById('pauseAll')?.addEventListener('click', () => {
pollingManager.activePollers.forEach((poller, id) => {
pollingManager.stopPolling(id);
});
this.showNotification('All polling paused', 'warning');
});
// Resume all button
document.getElementById('resumeAll')?.addEventListener('click', () => {
pollingManager.activePollers.forEach((poller, id) => {
if (!poller.isActive) {
pollingManager.startPolling(id);
}
});
this.showNotification('All polling resumed', 'success');
});
}
manualRefreshAll() {
console.log('🔄 Manual refresh triggered');
this.showNotification('Refreshing all data...', 'info');
pollingManager.activePollers.forEach((poller) => {
if (poller.isActive) {
// Clear current timeout and trigger immediate poll
clearTimeout(poller.timeoutId);
pollingManager.executePoll(poller);
}
});
}
initializeNotificationSystem() {
// Create notification container if it doesn't exist
if (!document.getElementById('notificationContainer')) {
const container = document.createElement('div');
container.id = 'notificationContainer';
container.className = 'notification-container';
document.body.appendChild(container);
}
}
showNotification(message, type = 'info', duration = 3000) {
const notification = {
id: Date.now(),
message,
type,
timestamp: new Date()
};
this.notifications.unshift(notification);
const notificationElement = this.createNotificationElement(notification);
document.getElementById('notificationContainer').appendChild(notificationElement);
// Auto-remove after duration
setTimeout(() => {
this.removeNotification(notification.id);
}, duration);
// Play sound if enabled
if (this.dashboardState.soundEnabled && type !== 'info') {
this.playNotificationSound(type);
}
}
createNotificationElement(notification) {
const element = document.createElement('div');
element.className = `notification notification-${notification.type}`;
element.id = `notification-${notification.id}`;
const icons = {
'success': '✅',
'warning': '⚠️',
'error': '❌',
'info': 'ℹ️'
};
element.innerHTML = `
<div class="notification-content">
<span class="notification-icon">${icons[notification.type]}</span>
<span class="notification-message">${notification.message}</span>
<span class="notification-time">${notification.timestamp.toLocaleTimeString()}</span>
</div>
<button class="notification-close" onclick="dashboard.removeNotification(${notification.id})">×</button>
`;
return element;
}
removeNotification(id) {
const element = document.getElementById(`notification-${id}`);
if (element) {
element.remove();
}
this.notifications = this.notifications.filter(n => n.id !== id);
}
playNotificationSound(type) {
// Simple audio feedback using Web Audio API
if ('AudioContext' in window || 'webkitAudioContext' in window) {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// Different frequencies for different notification types
const frequencies = {
'success': 800,
'warning': 600,
'error': 400
};
oscillator.frequency.setValueAtTime(frequencies[type] || 500, audioContext.currentTime);
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.3);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
}
}
setupDataAlerts(dataType, conditions) {
this.alerts.set(dataType, conditions);
}
checkAlert(dataType, data) {
const conditions = this.alerts.get(dataType);
if (!conditions) return;
conditions.forEach(condition => {
if (condition.check(data)) {
this.showNotification(
condition.message(data),
condition.severity || 'warning',
condition.duration || 5000
);
}
});
}
updateDashboardStats() {
const stats = pollingManager.getPollingStats();
document.getElementById('activePollers').textContent = stats.activePollers;
document.getElementById('totalPollers').textContent = stats.totalPollers;
// Update detailed stats table
const statsContainer = document.getElementById('pollingStats');
if (statsContainer) {
statsContainer.innerHTML = stats.pollerDetails.map(poller => `
<tr class="${poller.active ? 'active' : 'inactive'}">
<td>${poller.id}</td>
<td>${poller.dataType}</td>
<td>${poller.active ? '✅' : '❌'}</td>
<td>${(poller.interval / 1000).toFixed(1)}s</td>
<td>${poller.successCount}</td>
<td>${poller.errorCount}</td>
<td>${poller.avgResponseTime}ms</td>
<td>${poller.lastUpdate}</td>
</tr>
`).join('');
}
}
}
// Global dashboard instance
const dashboard = new LiveDashboard();
// Update dashboard stats every 5 seconds
setInterval(() => {
dashboard.updateDashboardStats();
}, 5000);
// Example alert setups for different data types
// ISS alerts
dashboard.setupDataAlerts('iss', [
{
check: (data) => Math.abs(data.latitude) > 60,
message: (data) => `ISS is at high latitude: ${data.latitude.toFixed(2)}°`,
severity: 'info'
},
{
check: (data) => data.longitude > 0 && data.longitude < 180,
message: (data) => 'ISS is over the Eastern Hemisphere',
severity: 'info'
}
]);
// Crypto alerts
dashboard.setupDataAlerts('crypto', [
{
check: (data) => data.some(crypto => Math.abs(crypto.change24h) > 10),
message: (data) => {
const bigMovers = data.filter(crypto => Math.abs(crypto.change24h) > 10);
return `Big price movement: ${bigMovers[0].symbol} ${bigMovers[0].change24h.toFixed(2)}%`;
},
severity: 'warning'
},
{
check: (data) => data.some(crypto => crypto.price > 50000), // Bitcoin over $50k
message: (data) => 'Bitcoin crossed $50,000!',
severity: 'success'
}
]);
// Pokemon team alerts
dashboard.setupDataAlerts('pokemon', [
{
check: (data) => data.averageStats.attack > 100,
message: (data) => `Strong offensive team! Average attack: ${data.averageStats.attack}`,
severity: 'success'
},
{
check: (data) => data.teamSize < 6,
message: (data) => `Team incomplete: ${data.teamSize}/6 Pokemon`,
severity: 'warning'
}
]);
Your task:
What you'll do: Create live updating displays Time: 10 minutes
class LiveDataVisualizer {
constructor() {
this.charts = new Map();
this.maxDataPoints = 50; // Keep last 50 data points
this.animationDuration = 750;
}
createTimeSeriesChart(containerId, options = {}) {
const canvas = document.getElementById(containerId);
if (!canvas) {
console.error(`Canvas ${containerId} not found`);
return null;
}
const chart = new Chart(canvas.getContext('2d'), {
type: 'line',
data: {
labels: [],
datasets: [{
label: options.label || 'Value',
data: [],
borderColor: options.color || 'rgb(75, 192, 192)',
backgroundColor: options.backgroundColor || 'rgba(75, 192, 192, 0.1)',
tension: 0.1,
pointRadius: 2,
pointHoverRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: this.animationDuration
},
scales: {
x: {
type: 'time',
time: {
displayFormats: {
second: 'HH:mm:ss'
}
},
title: {
display: true,
text: 'Time'
}
},
y: {
title: {
display: true,
text: options.yAxisLabel || 'Value'
}
}
},
plugins: {
legend: {
display: true
},
tooltip: {
mode: 'index',
intersect: false
}
}
}
});
this.charts.set(containerId, chart);
return chart;
}
updateChart(chartId, newValue, timestamp = Date.now()) {
const chart = this.charts.get(chartId);
if (!chart) return;
const data = chart.data;
const timeLabel = new Date(timestamp).toLocaleTimeString();
// Add new data point
data.labels.push(timeLabel);
data.datasets[0].data.push(newValue);
// Remove old data points if we exceed the limit
if (data.labels.length > this.maxDataPoints) {
data.labels.shift();
data.datasets[0].data.shift();
}
chart.update('none'); // Update without animation for real-time feel
}
createGaugeDisplay(containerId, options = {}) {
const container = document.getElementById(containerId);
if (!container) return;
const gaugeHTML = `
<div class="gauge-container">
<div class="gauge">
<div class="gauge-body">
<div class="gauge-fill"></div>
<div class="gauge-cover">
<div class="gauge-value">${options.initialValue || 0}</div>
<div class="gauge-label">${options.label || 'Value'}</div>
</div>
</div>
</div>
</div>
`;
container.innerHTML = gaugeHTML;
}
updateGauge(containerId, value, maxValue = 100) {
const container = document.getElementById(containerId);
if (!container) return;
const percentage = Math.min((value / maxValue) * 100, 100);
const fillElement = container.querySelector('.gauge-fill');
const valueElement = container.querySelector('.gauge-value');
if (fillElement && valueElement) {
fillElement.style.transform = `rotate(${(percentage / 100) * 180}deg)`;
valueElement.textContent = value.toFixed(1);
}
}
createLiveMap(containerId, options = {}) {
// Simple map implementation for ISS tracking
const container = document.getElementById(containerId);
if (!container) return;
const mapHTML = `
<div class="live-map">
<div class="map-grid">
<div class="iss-marker" id="iss-marker">🛰️</div>
</div>
<div class="map-coords">
<span>Lat: <span id="map-lat">0</span>°</span>
<span>Lng: <span id="map-lng">0</span>°</span>
</div>
</div>
`;
container.innerHTML = mapHTML;
}
updateMapPosition(containerId, latitude, longitude) {
const container = document.getElementById(containerId);
if (!container) return;
const marker = container.querySelector('#iss-marker');
const latSpan = container.querySelector('#map-lat');
const lngSpan = container.querySelector('#map-lng');
if (marker && latSpan && lngSpan) {
// Convert lat/lng to map coordinates (simplified)
const x = ((longitude + 180) / 360) * 100;
const y = ((90 - latitude) / 180) * 100;
marker.style.left = `${x}%`;
marker.style.top = `${y}%`;
latSpan.textContent = latitude.toFixed(4);
lngSpan.textContent = longitude.toFixed(4);
}
}
}
// Global visualizer instance
const visualizer = new LiveDataVisualizer();
// Example usage for ISS tracking
visualizer.createTimeSeriesChart('iss-altitude-chart', {
label: 'ISS Altitude',
color: 'rgb(54, 162, 235)',
yAxisLabel: 'Altitude (km)'
});
visualizer.createLiveMap('iss-map');
// Example usage for crypto prices
visualizer.createTimeSeriesChart('crypto-price-chart', {
label: 'Bitcoin Price',
color: 'rgb(255, 206, 84)',
yAxisLabel: 'Price (USD)'
});
Your task:
Try these scenarios:
Easy: Add data export functionality for historical data
Medium: Implement WebSocket connections where available
Hard: Create predictive analysis based on data trends
Fantastic work on building a comprehensive real-time data system! You've learned how to efficiently poll APIs, manage performance, and create engaging live experiences. In the next lessons, you'll start building your own APIs to complete the full-stack picture.
Polling not working?
Performance issues?
Charts not updating?
Alerts not triggering?