// 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 = `