// Mapbox Globe 6: Global University Rankings with Interactive Filtering // Web-enhanced learning from: https://docs.mapbox.com/mapbox-gl-js/example/filter-markers/ // Mapbox access token mapboxgl.accessToken = 'pk.eyJ1IjoibGludXhpc2Nvb2wiLCJhIjoiY2w3ajM1MnliMDV4NDNvb2J5c3V5dzRxZyJ9.wJukH5hVSiO74GM_VSJR3Q'; // Initialize map with globe projection const map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/dark-v11', projection: 'globe', center: [0, 20], zoom: 1.5, pitch: 0 }); // Auto-rotation state let userInteracting = false; let rotationPaused = false; // Current filter state let currentFilters = { regions: ['all'], rankingRange: [1, 120], minPublications: 0, minCitations: 0, minFunding: 0, minNobelPrizes: 0 }; // Map load event map.on('load', () => { // Configure globe atmosphere map.setFog({ color: 'rgba(8, 16, 32, 0.95)', 'high-color': 'rgba(25, 60, 120, 0.5)', 'horizon-blend': 0.3, 'space-color': '#000510', 'star-intensity': 0.7 }); // Add data source map.addSource('universities', { type: 'geojson', data: universitiesData }); // Layer 1: University circles (main visualization) map.addLayer({ id: 'universities-layer', type: 'circle', source: 'universities', paint: { // Circle color based on research score (data-driven) 'circle-color': [ 'interpolate', ['linear'], ['get', 'researchScore'], 55, '#8b0000', // Deep red - Lower scores 65, '#dc143c', // Crimson 75, '#ff6347', // Tomato red 80, '#ffa500', // Orange 85, '#ffd700', // Gold 90, '#32cd32', // Lime green 95, '#00bfff', // Deep sky blue 99, '#0066ff' // Royal blue - Top scores ], // Circle radius based on ranking (inverse - better rank = larger) 'circle-radius': [ 'interpolate', ['linear'], ['get', 'ranking'], 1, 18, // Top rank = largest 10, 14, 25, 11, 50, 9, 75, 7, 100, 5, 120, 4 // Lower rank = smallest ], 'circle-opacity': 0.85, 'circle-stroke-width': 2, 'circle-stroke-color': [ 'interpolate', ['linear'], ['get', 'researchScore'], 55, '#ff4444', 80, '#ffaa00', 95, '#00ffff' ], 'circle-stroke-opacity': 0.6 } }); // Layer 2: University labels (for top 30) map.addLayer({ id: 'universities-labels', type: 'symbol', source: 'universities', filter: ['<=', ['get', 'ranking'], 30], // Only show labels for top 30 layout: { 'text-field': ['get', 'name'], 'text-font': ['Open Sans Regular'], 'text-size': [ 'interpolate', ['linear'], ['get', 'ranking'], 1, 13, 10, 11, 30, 9 ], 'text-offset': [0, 1.5], 'text-anchor': 'top', 'text-allow-overlap': false, 'text-optional': true }, paint: { 'text-color': '#ffffff', 'text-halo-color': '#000000', 'text-halo-width': 2, 'text-opacity': 0.9 } }); // Add navigation control map.addControl(new mapboxgl.NavigationControl(), 'bottom-right'); // Setup popups setupPopups(); // Setup filter controls setupFilterControls(); // Start auto-rotation startAutoRotation(); // Update statistics updateStatistics(); }); // Setup popup interactions function setupPopups() { // Create popup const popup = new mapboxgl.Popup({ closeButton: false, closeOnClick: false }); // Mouse enter map.on('mouseenter', 'universities-layer', (e) => { map.getCanvas().style.cursor = 'pointer'; const coords = e.features[0].geometry.coordinates.slice(); const props = e.features[0].properties; const html = `

${props.name}

Country: ${props.country}
Region: ${props.region}
Global Ranking: #${props.ranking}
Research Score: ${props.researchScore}/100

Publications: ${props.publications.toLocaleString()}
Citations: ${props.citations.toLocaleString()}
Research Funding: $${props.funding}M
Nobel Prizes: ${props.nobelPrizes}
`; popup.setLngLat(coords).setHTML(html).addTo(map); }); // Mouse leave map.on('mouseleave', 'universities-layer', () => { map.getCanvas().style.cursor = ''; popup.remove(); }); } // Setup filter controls (Web-enhanced from Mapbox filter-markers example) function setupFilterControls() { // Region filter checkboxes const regionFilters = document.getElementById('region-filters'); const regions = ['All', 'North America', 'Europe', 'Asia-Pacific', 'Middle East', 'Africa', 'Latin America']; regions.forEach((region, index) => { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `region-${index}`; checkbox.value = region; checkbox.checked = region === 'All'; const label = document.createElement('label'); label.htmlFor = `region-${index}`; label.textContent = region; const wrapper = document.createElement('div'); wrapper.className = 'filter-item'; wrapper.appendChild(checkbox); wrapper.appendChild(label); regionFilters.appendChild(wrapper); // Event listener using visibility control pattern from web source checkbox.addEventListener('change', () => { if (region === 'All') { // If "All" is checked, uncheck others if (checkbox.checked) { regions.slice(1).forEach((_, i) => { document.getElementById(`region-${i + 1}`).checked = false; }); } } else { // If any specific region is checked, uncheck "All" document.getElementById('region-0').checked = false; } applyFilters(); }); }); // Ranking range slider const rankingSlider = document.getElementById('ranking-slider'); const rankingValue = document.getElementById('ranking-value'); rankingSlider.addEventListener('input', (e) => { currentFilters.rankingRange = [1, parseInt(e.target.value)]; rankingValue.textContent = `Top ${e.target.value}`; applyFilters(); }); // Publications slider const pubsSlider = document.getElementById('publications-slider'); const pubsValue = document.getElementById('publications-value'); pubsSlider.addEventListener('input', (e) => { currentFilters.minPublications = parseInt(e.target.value); pubsValue.textContent = `≥${(parseInt(e.target.value) / 1000).toFixed(0)}K`; applyFilters(); }); // Citations slider const citationsSlider = document.getElementById('citations-slider'); const citationsValue = document.getElementById('citations-value'); citationsSlider.addEventListener('input', (e) => { currentFilters.minCitations = parseInt(e.target.value); citationsValue.textContent = `≥${(parseInt(e.target.value) / 1000).toFixed(0)}K`; applyFilters(); }); // Funding slider const fundingSlider = document.getElementById('funding-slider'); const fundingValue = document.getElementById('funding-value'); fundingSlider.addEventListener('input', (e) => { currentFilters.minFunding = parseInt(e.target.value); fundingValue.textContent = `≥$${e.target.value}M`; applyFilters(); }); // Nobel prize slider const nobelSlider = document.getElementById('nobel-slider'); const nobelValue = document.getElementById('nobel-value'); nobelSlider.addEventListener('input', (e) => { currentFilters.minNobelPrizes = parseInt(e.target.value); nobelValue.textContent = `≥${e.target.value}`; applyFilters(); }); // Reset button document.getElementById('reset-filters').addEventListener('click', () => { // Reset all checkboxes document.getElementById('region-0').checked = true; regions.slice(1).forEach((_, i) => { document.getElementById(`region-${i + 1}`).checked = false; }); // Reset sliders rankingSlider.value = 120; pubsSlider.value = 0; citationsSlider.value = 0; fundingSlider.value = 0; nobelSlider.value = 0; // Reset filter state currentFilters = { regions: ['all'], rankingRange: [1, 120], minPublications: 0, minCitations: 0, minFunding: 0, minNobelPrizes: 0 }; // Update displays rankingValue.textContent = 'Top 120'; pubsValue.textContent = '≥0K'; citationsValue.textContent = '≥0K'; fundingValue.textContent = '≥$0M'; nobelValue.textContent = '≥0'; applyFilters(); }); } // Apply filters using Mapbox filter expressions (learned from web source) function applyFilters() { // Get selected regions const selectedRegions = []; const checkboxes = document.querySelectorAll('#region-filters input[type="checkbox"]:checked'); checkboxes.forEach(cb => { if (cb.value === 'All') { selectedRegions.push('all'); } else { selectedRegions.push(cb.value); } }); // Build filter expression using 'all' operator to combine conditions let filterExpression = ['all']; // Region filter (using 'in' operator for multiple values) if (!selectedRegions.includes('all') && selectedRegions.length > 0) { filterExpression.push([ 'in', ['get', 'region'], ['literal', selectedRegions] ]); } // Ranking range filter filterExpression.push([ '<=', ['get', 'ranking'], currentFilters.rankingRange[1] ]); // Publications filter if (currentFilters.minPublications > 0) { filterExpression.push([ '>=', ['get', 'publications'], currentFilters.minPublications ]); } // Citations filter if (currentFilters.minCitations > 0) { filterExpression.push([ '>=', ['get', 'citations'], currentFilters.minCitations ]); } // Funding filter if (currentFilters.minFunding > 0) { filterExpression.push([ '>=', ['get', 'funding'], currentFilters.minFunding ]); } // Nobel prizes filter if (currentFilters.minNobelPrizes > 0) { filterExpression.push([ '>=', ['get', 'nobelPrizes'], currentFilters.minNobelPrizes ]); } // Apply filter to layer using setFilter method (from web source) map.setFilter('universities-layer', filterExpression); // Also update labels layer to match const labelFilter = ['all', filterExpression, ['<=', ['get', 'ranking'], 30]]; map.setFilter('universities-labels', labelFilter); // Update statistics with filtered data updateStatistics(); } // Update statistics display function updateStatistics() { // Get currently visible features const features = map.queryRenderedFeatures({ layers: ['universities-layer'] }); if (features.length > 0) { const count = features.length; const avgResearch = (features.reduce((sum, f) => sum + f.properties.researchScore, 0) / count).toFixed(1); const totalPubs = features.reduce((sum, f) => sum + f.properties.publications, 0); const totalCitations = features.reduce((sum, f) => sum + f.properties.citations, 0); const totalNobel = features.reduce((sum, f) => sum + f.properties.nobelPrizes, 0); document.getElementById('stat-total').textContent = count; document.getElementById('stat-research').textContent = avgResearch; document.getElementById('stat-publications').textContent = (totalPubs / 1000).toFixed(1) + 'K'; document.getElementById('stat-citations').textContent = (totalCitations / 1000000).toFixed(1) + 'M'; document.getElementById('stat-nobel').textContent = totalNobel; } } // Auto-rotation logic function startAutoRotation() { map.on('mousedown', () => { userInteracting = true; }); map.on('mouseup', () => { userInteracting = false; }); map.on('dragstart', () => { userInteracting = true; }); map.on('dragend', () => { userInteracting = false; }); map.on('pitchstart', () => { userInteracting = true; }); map.on('pitchend', () => { userInteracting = false; }); function rotateGlobe() { if (!userInteracting && !rotationPaused) { const center = map.getCenter(); center.lng += 0.05; if (center.lng > 180) center.lng = -180; map.setCenter(center); } requestAnimationFrame(rotateGlobe); } rotateGlobe(); } // Toggle rotation button document.getElementById('toggle-rotation').addEventListener('click', () => { rotationPaused = !rotationPaused; const btn = document.getElementById('toggle-rotation'); btn.textContent = rotationPaused ? 'Resume Rotation' : 'Pause Rotation'; });