// Mapbox Globe Iteration 7: Timeline Animation for Online Education Growth // Learning from: https://docs.mapbox.com/mapbox-gl-js/example/timeline-animation/ // Implementation: Range slider timeline control for temporal data exploration // Mapbox access token mapboxgl.accessToken = 'pk.eyJ1IjoibGludXhpc2Nvb2wiLCJhIjoiY2w3ajM1MnliMDV4NDNvb2J5c3V5dzRxZyJ9.wJukH5hVSiO74GM_VSJR3Q'; // Timeline years for the visualization const years = ['2010', '2012', '2014', '2016', '2018', '2020', '2022', '2024']; let currentYearIndex = 0; let isPlaying = false; let playInterval = null; // Initialize the map with globe projection const map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/dark-v11', projection: 'globe', center: [20, 20], zoom: 1.8, pitch: 20 }); // Process data: Transform yearData into separate features per year // This is inspired by the timeline example's approach to preprocessing temporal data function processTemporalData() { const processedFeatures = []; educationData.features.forEach(feature => { years.forEach((year, yearIndex) => { const yearStats = feature.properties.yearData[year]; if (yearStats && yearStats.enrollment > 0) { processedFeatures.push({ type: 'Feature', geometry: feature.geometry, properties: { name: feature.properties.name, type: feature.properties.type, country: feature.properties.country, year: year, yearIndex: yearIndex, enrollment: yearStats.enrollment, courses: yearStats.courses, completionRate: yearStats.completionRate, // Calculate growth metrics growthRate: calculateGrowthRate(feature.properties.yearData, year, yearIndex) } }); } }); }); return { type: 'FeatureCollection', features: processedFeatures }; } // Calculate year-over-year growth rate function calculateGrowthRate(yearData, currentYear, yearIndex) { if (yearIndex === 0) return 0; const prevYear = years[yearIndex - 1]; const currentEnrollment = yearData[currentYear].enrollment; const prevEnrollment = yearData[prevYear].enrollment; if (prevEnrollment === 0) return 100; return Math.round(((currentEnrollment - prevEnrollment) / prevEnrollment) * 100); } // Filter features by year index (inspired by timeline example's filterBy function) function filterByYear(yearIndex) { map.setFilter('education-circles', ['==', 'yearIndex', yearIndex]); map.setFilter('education-labels', ['==', 'yearIndex', yearIndex]); // Update the timeline display document.getElementById('year-display').textContent = years[yearIndex]; document.getElementById('year-slider').value = yearIndex; // Update statistics updateStatistics(yearIndex); } // Calculate and display aggregate statistics for the selected year function updateStatistics(yearIndex) { const year = years[yearIndex]; let totalEnrollment = 0; let totalCourses = 0; let platformCount = 0; let totalCompletionRate = 0; educationData.features.forEach(feature => { const yearStats = feature.properties.yearData[year]; if (yearStats && yearStats.enrollment > 0) { totalEnrollment += yearStats.enrollment; totalCourses += yearStats.courses; platformCount++; totalCompletionRate += yearStats.completionRate; } }); const avgCompletionRate = platformCount > 0 ? Math.round(totalCompletionRate / platformCount) : 0; document.getElementById('total-enrollment').textContent = formatNumber(totalEnrollment); document.getElementById('total-courses').textContent = formatNumber(totalCourses); document.getElementById('platform-count').textContent = platformCount; document.getElementById('avg-completion').textContent = avgCompletionRate + '%'; } // Format large numbers for display function formatNumber(num) { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toString(); } // Play/pause animation through timeline function togglePlayback() { isPlaying = !isPlaying; const playButton = document.getElementById('play-button'); if (isPlaying) { playButton.textContent = 'Pause'; playInterval = setInterval(() => { currentYearIndex = (currentYearIndex + 1) % years.length; filterByYear(currentYearIndex); }, 1500); // Advance every 1.5 seconds } else { playButton.textContent = 'Play'; if (playInterval) { clearInterval(playInterval); playInterval = null; } } } map.on('load', () => { // Configure globe atmosphere map.setFog({ color: 'rgba(12, 20, 39, 0.9)', 'high-color': 'rgba(36, 92, 223, 0.4)', 'horizon-blend': 0.4, 'space-color': '#000814', 'star-intensity': 0.7 }); // Process temporal data const processedData = processTemporalData(); // Add data source map.addSource('education-data', { type: 'geojson', data: processedData }); // Add circle layer for education platforms // Size and color based on enrollment and growth map.addLayer({ id: 'education-circles', type: 'circle', source: 'education-data', paint: { // Size based on enrollment 'circle-radius': [ 'interpolate', ['exponential', 1.5], ['get', 'enrollment'], 0, 3, 1000000, 8, 10000000, 14, 50000000, 22, 150000000, 32 ], // Color based on growth rate 'circle-color': [ 'interpolate', ['linear'], ['get', 'growthRate'], -50, '#d73027', // Decline - red 0, '#fee08b', // No growth - yellow 50, '#a6d96a', // Moderate growth - light green 100, '#1a9850', // Strong growth - green 200, '#0571b0' // Exceptional growth - blue ], 'circle-opacity': 0.85, 'circle-stroke-width': 2, 'circle-stroke-color': [ 'case', ['>=', ['get', 'enrollment'], 10000000], '#ffffff', 'rgba(255, 255, 255, 0.5)' ] } }); // Add labels for major platforms map.addLayer({ id: 'education-labels', type: 'symbol', source: 'education-data', filter: ['>=', ['get', 'enrollment'], 5000000], // Only label platforms with 5M+ enrollments layout: { 'text-field': ['get', 'name'], 'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'], 'text-size': 11, 'text-offset': [0, 1.5], 'text-anchor': 'top', 'text-optional': true }, paint: { 'text-color': '#ffffff', 'text-halo-color': '#000000', 'text-halo-width': 1.5 } }); // Add popups on hover const popup = new mapboxgl.Popup({ closeButton: false, closeOnClick: false, offset: 15 }); map.on('mouseenter', 'education-circles', (e) => { map.getCanvas().style.cursor = 'pointer'; const props = e.features[0].properties; const coords = e.features[0].geometry.coordinates.slice(); const html = `
${props.type} - ${props.country}