// Mapbox Globe Visualization 5: Global Educational Institutions
// Demonstrates data-driven styling with match expressions for categorical data
// and interpolate expressions for continuous educational metrics
// 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: [15, 25],
zoom: 1.8,
pitch: 0
});
// Auto-rotation state
let userInteracting = false;
let rotationActive = true;
// Track current styling metric
let currentSizeMetric = 'enrollment';
let currentColorMetric = 'quality';
map.on('load', () => {
// Configure globe atmosphere
map.setFog({
color: 'rgba(20, 30, 50, 0.9)',
'high-color': 'rgba(50, 80, 150, 0.5)',
'horizon-blend': 0.05,
'space-color': 'rgba(5, 5, 15, 1)',
'star-intensity': 0.7
});
// Add educational institutions data source
map.addSource('education', {
type: 'geojson',
data: educationData
});
// Main institution layer - using MATCH expression for categorical type styling
// and INTERPOLATE for continuous quality metrics
// This demonstrates the data-driven styling learned from Mapbox documentation
map.addLayer({
id: 'institutions',
type: 'circle',
source: 'education',
paint: {
// SIZE: Interpolate based on enrollment (continuous data)
'circle-radius': [
'interpolate',
['linear'],
['get', 'enrollment'],
1000, 4, // Small institutions
10000, 7,
30000, 11,
60000, 16,
100000, 22,
350000, 30 // Massive universities
],
// COLOR: Interpolate based on quality score (continuous data)
// Uses a diverging-like scale from red (low quality) to gold (high quality)
'circle-color': [
'interpolate',
['linear'],
['get', 'quality'],
50, '#8b0000', // Dark red - very low quality
60, '#dc143c', // Crimson - low quality
70, '#ff6347', // Tomato - below average
75, '#ff8c00', // Dark orange - average
80, '#ffa500', // Orange - above average
85, '#ffd700', // Gold - good
90, '#00ced1', // Dark turquoise - very good
95, '#00bfff', // Deep sky blue - excellent
100, '#1e90ff' // Dodger blue - world class
],
// OPACITY: Zoom-responsive for better visibility
'circle-opacity': [
'interpolate',
['linear'],
['zoom'],
1, 0.75,
4, 0.85,
8, 0.95
],
// STROKE: Categorical styling using MATCH expression
// This is the key technique learned from the web source
'circle-stroke-color': [
'match',
['get', 'type'],
'University', '#ffffff',
'School', '#cccccc',
'#999999' // default
],
'circle-stroke-width': [
'interpolate',
['linear'],
['zoom'],
1, 0.5,
4, 1,
8, 2
]
}
});
// Labels layer for top-tier institutions (quality >= 85)
map.addLayer({
id: 'institution-labels',
type: 'symbol',
source: 'education',
filter: ['>=', ['get', 'quality'], 85],
layout: {
'text-field': ['get', 'name'],
'text-size': [
'interpolate',
['linear'],
['get', 'quality'],
85, 10,
100, 14
],
'text-offset': [0, 1.5],
'text-anchor': 'top',
'text-max-width': 8
},
paint: {
'text-color': '#ffffff',
'text-halo-color': '#000000',
'text-halo-width': 1.5,
'text-opacity': [
'interpolate',
['linear'],
['zoom'],
1, 0,
3, 0.6,
6, 1
]
},
minzoom: 3
});
// Interaction: Popup on hover
const popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false,
offset: 15
});
map.on('mouseenter', 'institutions', (e) => {
map.getCanvas().style.cursor = 'pointer';
const props = e.features[0].properties;
const coordinates = e.features[0].geometry.coordinates.slice();
const html = `
${props.name}
Country: ${props.country}
Type: ${props.type}
Quality Score: ${props.quality}/100
Enrollment: ${props.enrollment.toLocaleString()}
Literacy Rate: ${props.literacy}%
Funding: $${props.funding}M USD
`;
popup.setLngLat(coordinates).setHTML(html).addTo(map);
});
map.on('mouseleave', 'institutions', () => {
map.getCanvas().style.cursor = '';
popup.remove();
});
// Start auto-rotation
startRotation();
// Update statistics panel
updateStatistics();
});
// Auto-rotation function
function startRotation() {
if (!rotationActive || userInteracting) return;
const center = map.getCenter();
center.lng -= 0.08; // Slower rotation for educational theme
map.easeTo({
center: center,
duration: 1000,
easing: (t) => t
});
}
// Rotation loop
map.on('moveend', () => {
if (rotationActive) {
startRotation();
}
});
// Pause rotation on user interaction
map.on('mousedown', () => { userInteracting = true; });
map.on('mouseup', () => {
userInteracting = false;
if (rotationActive) startRotation();
});
map.on('dragstart', () => { userInteracting = true; });
map.on('dragend', () => {
userInteracting = false;
if (rotationActive) startRotation();
});
map.on('pitchstart', () => { userInteracting = true; });
map.on('pitchend', () => {
userInteracting = false;
if (rotationActive) startRotation();
});
// Toggle rotation
document.getElementById('toggle-rotation').addEventListener('click', () => {
rotationActive = !rotationActive;
document.getElementById('toggle-rotation').textContent =
rotationActive ? 'Pause Rotation' : 'Resume Rotation';
if (rotationActive) startRotation();
});
// Reset view
document.getElementById('reset-view').addEventListener('click', () => {
map.flyTo({
center: [15, 25],
zoom: 1.8,
pitch: 0,
bearing: 0,
duration: 2000
});
});
// Metric switching - demonstrates dynamic expression updates
document.getElementById('size-metric').addEventListener('change', (e) => {
currentSizeMetric = e.target.value;
updateCircleSize();
updateLegend();
});
document.getElementById('color-metric').addEventListener('change', (e) => {
currentColorMetric = e.target.value;
updateCircleColor();
updateLegend();
});
// Update circle size based on selected metric
function updateCircleSize() {
const sizeExpressions = {
enrollment: [
'interpolate',
['linear'],
['get', 'enrollment'],
1000, 4,
10000, 7,
30000, 11,
60000, 16,
100000, 22,
350000, 30
],
quality: [
'interpolate',
['linear'],
['get', 'quality'],
50, 4,
60, 6,
70, 9,
80, 13,
90, 18,
100, 26
],
literacy: [
'interpolate',
['linear'],
['get', 'literacy'],
40, 4,
60, 8,
75, 12,
90, 17,
100, 24
],
funding: [
'interpolate',
['linear'],
['get', 'funding'],
200, 4,
500, 7,
1000, 10,
2000, 14,
3500, 19,
5500, 28
]
};
map.setPaintProperty('institutions', 'circle-radius', sizeExpressions[currentSizeMetric]);
}
// Update circle color based on selected metric
function updateCircleColor() {
const colorExpressions = {
quality: [
'interpolate',
['linear'],
['get', 'quality'],
50, '#8b0000',
60, '#dc143c',
70, '#ff6347',
75, '#ff8c00',
80, '#ffa500',
85, '#ffd700',
90, '#00ced1',
95, '#00bfff',
100, '#1e90ff'
],
literacy: [
'interpolate',
['linear'],
['get', 'literacy'],
40, '#8b0000',
50, '#dc143c',
65, '#ff6347',
75, '#ffa500',
85, '#ffd700',
92, '#00ced1',
97, '#00bfff',
100, '#1e90ff'
],
enrollment: [
'interpolate',
['linear'],
['get', 'enrollment'],
1000, '#4a148c', // Deep purple - small
10000, '#7b1fa2', // Purple
30000, '#9c27b0', // Medium purple
60000, '#ba68c8', // Light purple
100000, '#ce93d8', // Very light purple
350000, '#e1bee7' // Pale purple - massive
],
funding: [
'interpolate',
['linear'],
['get', 'funding'],
200, '#1a5490', // Dark blue - low funding
500, '#2874a6',
1000, '#3498db', // Medium blue
2000, '#5dade2',
3500, '#85c1e9',
5500, '#aed6f1' // Light blue - high funding
]
};
map.setPaintProperty('institutions', 'circle-color', colorExpressions[currentColorMetric]);
}
// Update statistics panel
function updateStatistics() {
document.getElementById('total-institutions').textContent = globalStats.totalInstitutions;
document.getElementById('avg-quality').textContent = globalStats.avgQuality;
document.getElementById('avg-literacy').textContent = globalStats.avgLiteracy + '%';
document.getElementById('total-enrollment').textContent =
(globalStats.totalEnrollment / 1000000).toFixed(1) + 'M';
}
// Update legend based on current metrics
function updateLegend() {
const sizeLabels = {
enrollment: { unit: ' students', min: '1K', max: '350K' },
quality: { unit: '/100', min: '50', max: '100' },
literacy: { unit: '%', min: '40', max: '100' },
funding: { unit: 'M USD', min: '200', max: '5500' }
};
const colorLabels = {
quality: { min: 'Low Quality (50)', max: 'World Class (100)' },
literacy: { min: 'Low Literacy (40%)', max: 'Universal (100%)' },
enrollment: { min: 'Small (1K)', max: 'Massive (350K)' },
funding: { min: 'Low Funded ($200M)', max: 'High Funded ($5.5B)' }
};
const sizeLabel = sizeLabels[currentSizeMetric];
const colorLabel = colorLabels[currentColorMetric];
document.getElementById('size-min-label').textContent = sizeLabel.min;
document.getElementById('size-max-label').textContent = sizeLabel.max;
document.getElementById('color-min-label').textContent = colorLabel.min;
document.getElementById('color-max-label').textContent = colorLabel.max;
}
// Add navigation controls
map.addControl(new mapboxgl.NavigationControl(), 'bottom-right');
map.addControl(new mapboxgl.FullscreenControl(), 'bottom-right');
// Initialize legend
updateLegend();