// 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';
});