// Mapbox Globe Iteration 9: Global Educational Funding Choropleth // Applying choropleth techniques learned from Mapbox documentation // 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: [20, 20], zoom: 1.8, pitch: 0 }); // Data processing: calculate funding metrics const processedData = educationalFundingData.features.map(feature => { const props = feature.properties; return { ...feature, properties: { ...props, fundingGap: props.spendingPerCapita < 500 ? 'Critical' : props.spendingPerCapita < 1000 ? 'High' : props.spendingPerCapita < 2000 ? 'Moderate' : 'Low', efficiency: props.spendingPerCapita > 0 ? (props.educationIndex / (props.spendingPerCapita / 1000)).toFixed(2) : 0, salaryGap: props.avgTeacherSalary < 10000 ? 'Low' : props.avgTeacherSalary < 30000 ? 'Medium' : props.avgTeacherSalary < 50000 ? 'Good' : 'High' } }; }); // Country rankings by spending const countryRankings = [...processedData].sort( (a, b) => b.properties.spendingPerCapita - a.properties.spendingPerCapita ); // Statistics const stats = { avgSpending: processedData.reduce((sum, f) => sum + f.properties.spendingPerCapita, 0) / processedData.length, maxSpending: Math.max(...processedData.map(f => f.properties.spendingPerCapita)), minSpending: Math.min(...processedData.map(f => f.properties.spendingPerCapita)), totalCenters: trainingCentersData.features.length, avgTeacherSalary: processedData.reduce((sum, f) => sum + f.properties.avgTeacherSalary, 0) / processedData.length }; // Update info panel document.getElementById('avg-spending').textContent = `$${stats.avgSpending.toFixed(0)}`; document.getElementById('total-countries').textContent = processedData.length; document.getElementById('total-centers').textContent = stats.totalCenters; document.getElementById('avg-salary').textContent = `$${stats.avgTeacherSalary.toFixed(0)}`; map.on('load', () => { // Configure globe atmosphere map.setFog({ color: 'rgba(10, 15, 35, 0.9)', 'high-color': 'rgba(25, 50, 100, 0.5)', 'horizon-blend': 0.3, 'space-color': '#000814', 'star-intensity': 0.7 }); // Add country polygons source from Mapbox // We'll use a custom source with our data for choropleth map.addSource('countries', { type: 'vector', url: 'mapbox://mapbox.country-boundaries-v1' }); // Create a lookup map for quick access to country data const countryDataMap = new Map(); processedData.forEach(feature => { countryDataMap.set(feature.properties.iso, feature.properties); }); // Add choropleth fill layer using data-driven styling // Technique learned from Mapbox documentation: interpolate expression for continuous color scaling map.addLayer({ 'id': 'country-choropleth', 'type': 'fill', 'source': 'countries', 'source-layer': 'country_boundaries', 'paint': { // Choropleth fill-color using interpolate expression // This creates a smooth color gradient based on data values 'fill-color': [ 'case', ['has', 'iso_3166_1'], [ 'interpolate', ['linear'], // Access spending data by matching ISO codes ['get', 'spendingPerCapita', ['literal', Object.fromEntries(countryDataMap)]], 0, '#440154', // Very low - deep purple 500, '#31688e', // Low - blue 1000, '#35b779', // Medium - green 2000, '#fde724', // High - yellow 5000, '#fde724' // Very high - bright yellow ], '#1a1a1a' // Default for countries without data ], 'fill-opacity': 0.8 } }, 'admin-1-boundary-bg'); // However, the above approach won't work directly because we can't join external data to vector tiles easily // Let's use a different approach: convert our point data to a GeoJSON with polygons // For this iteration, we'll create country fills from our point data // Add educational funding data source (points) map.addSource('education-data', { type: 'geojson', data: { type: 'FeatureCollection', features: processedData } }); // Add circle layer with size based on spending map.addLayer({ 'id': 'education-circles', 'type': 'circle', 'source': 'education-data', 'paint': { // Circle radius based on spending per capita 'circle-radius': [ 'interpolate', ['linear'], ['get', 'spendingPerCapita'], 0, 3, 500, 8, 1000, 12, 2000, 18, 5000, 25 ], // Choropleth-style color using interpolate (learned technique) 'circle-color': [ 'interpolate', ['linear'], ['get', 'spendingPerCapita'], 0, '#440154', // Very low - deep purple (viridis scale) 500, '#31688e', // Low - blue 1000, '#35b779', // Medium - green 2000, '#fde724', // High - yellow 5000, '#fde724' // Very high - bright yellow ], 'circle-opacity': 0.85, 'circle-stroke-width': 2, 'circle-stroke-color': [ 'interpolate', ['linear'], ['get', 'educationIndex'], 0.3, '#ff0000', // Low quality - red 0.6, '#ffaa00', // Medium quality - orange 0.8, '#00ff00' // High quality - green ], 'circle-stroke-opacity': 0.9 } }); // Add teacher training centers map.addSource('training-centers', { type: 'geojson', data: trainingCentersData }); // Training centers as small points map.addLayer({ 'id': 'training-centers-layer', 'type': 'circle', 'source': 'training-centers', 'paint': { 'circle-radius': 4, 'circle-color': '#ff6b6b', 'circle-opacity': 0.7, 'circle-stroke-width': 1, 'circle-stroke-color': '#ffffff', 'circle-stroke-opacity': 0.8 } }); // Add halo effect for training centers map.addLayer({ 'id': 'training-centers-halo', 'type': 'circle', 'source': 'training-centers', 'paint': { 'circle-radius': 8, 'circle-color': '#ff6b6b', 'circle-opacity': 0.2, 'circle-blur': 0.8 } }, 'training-centers-layer'); // Country labels layer map.addLayer({ 'id': 'country-labels', 'type': 'symbol', 'source': 'education-data', 'layout': { 'text-field': ['get', 'country'], 'text-size': 11, 'text-offset': [0, 2], 'text-anchor': 'top', 'text-optional': true }, 'paint': { 'text-color': '#ffffff', 'text-halo-color': '#000000', 'text-halo-width': 1.5, 'text-opacity': [ 'interpolate', ['linear'], ['zoom'], 1.5, 0, 2.5, 0.7 ] } }); // Create popup const popup = new mapboxgl.Popup({ closeButton: false, closeOnClick: false, maxWidth: '350px' }); // Hover interaction for countries map.on('mousemove', 'education-circles', (e) => { map.getCanvas().style.cursor = 'pointer'; const props = e.features[0].properties; const coordinates = e.features[0].geometry.coordinates.slice(); // Calculate additional metrics const fundingRank = countryRankings.findIndex(f => f.properties.country === props.country) + 1; const percentOfMax = ((props.spendingPerCapita / stats.maxSpending) * 100).toFixed(1); const html = `

${props.country}

Spending/Capita
$${props.spendingPerCapita}
Rank: #${fundingRank} (${percentOfMax}% of max)
Budget %
${props.budgetPercent}%
Avg Teacher Salary
$${props.avgTeacherSalary.toLocaleString()}
Teacher:Student
1:${props.teacherStudentRatio}
Education Index
${props.educationIndex}
Training Centers
${props.trainingCenters}
Funding Gap: ${props.fundingGap}
Efficiency Score: ${props.efficiency}
`; popup.setLngLat(coordinates).setHTML(html).addTo(map); }); map.on('mouseleave', 'education-circles', () => { map.getCanvas().style.cursor = ''; popup.remove(); }); // Hover interaction for training centers map.on('mousemove', 'training-centers-layer', (e) => { map.getCanvas().style.cursor = 'pointer'; const props = e.features[0].properties; const coordinates = e.features[0].geometry.coordinates.slice(); const html = `

Teacher Training Center

Country: ${props.country}
Capacity: ${props.capacity} students
Program: ${props.programTypes}
Est: ${props.yearEstablished}
`; popup.setLngLat(coordinates).setHTML(html).addTo(map); }); map.on('mouseleave', 'training-centers-layer', () => { map.getCanvas().style.cursor = ''; popup.remove(); }); // Click to fly to country map.on('click', 'education-circles', (e) => { const coordinates = e.features[0].geometry.coordinates; map.flyTo({ center: coordinates, zoom: 4, duration: 2000, pitch: 45, bearing: 0 }); }); // Slow rotation animation let userInteracting = false; const rotationSpeed = 0.15; const rotateCamera = (timestamp) => { if (!userInteracting) { map.rotateTo((timestamp / 200) % 360, { duration: 0 }); } requestAnimationFrame(rotateCamera); }; map.on('mousedown', () => { userInteracting = true; }); map.on('mouseup', () => { userInteracting = false; }); map.on('dragend', () => { userInteracting = false; }); map.on('pitchend', () => { userInteracting = false; }); map.on('rotateend', () => { userInteracting = false; }); // Start rotation rotateCamera(0); // Layer toggle functionality document.getElementById('toggle-centers').addEventListener('click', (e) => { const visibility = map.getLayoutProperty('training-centers-layer', 'visibility'); if (visibility === 'visible') { map.setLayoutProperty('training-centers-layer', 'visibility', 'none'); map.setLayoutProperty('training-centers-halo', 'visibility', 'none'); e.target.textContent = 'Show Training Centers'; e.target.classList.remove('active'); } else { map.setLayoutProperty('training-centers-layer', 'visibility', 'visible'); map.setLayoutProperty('training-centers-halo', 'visibility', 'visible'); e.target.textContent = 'Hide Training Centers'; e.target.classList.add('active'); } }); document.getElementById('toggle-labels').addEventListener('click', (e) => { const visibility = map.getLayoutProperty('country-labels', 'visibility'); if (visibility === 'visible') { map.setLayoutProperty('country-labels', 'visibility', 'none'); e.target.textContent = 'Show Labels'; e.target.classList.remove('active'); } else { map.setLayoutProperty('country-labels', 'visibility', 'visible'); e.target.textContent = 'Hide Labels'; e.target.classList.add('active'); } }); // Populate top countries list const topList = document.getElementById('top-countries-list'); countryRankings.slice(0, 10).forEach((feature, index) => { const props = feature.properties; const item = document.createElement('div'); item.className = 'country-item'; item.innerHTML = ` #${index + 1} ${props.country} $${props.spendingPerCapita} `; item.addEventListener('click', () => { map.flyTo({ center: feature.geometry.coordinates, zoom: 4, duration: 2000, pitch: 45 }); }); topList.appendChild(item); }); console.log('Globe loaded successfully!'); console.log(`Total countries: ${processedData.length}`); console.log(`Total training centers: ${stats.totalCenters}`); console.log(`Average spending: $${stats.avgSpending.toFixed(2)}`); console.log(`Top 5 countries by spending:`, countryRankings.slice(0, 5).map(f => `${f.properties.country} ($${f.properties.spendingPerCapita})` )); }); // Add navigation controls map.addControl(new mapboxgl.NavigationControl(), 'top-right'); // Add fullscreen control map.addControl(new mapboxgl.FullscreenControl(), 'top-right');