// Mapbox Globe Iteration 8: Educational Infrastructure with Clustering // Web Research Source: https://docs.mapbox.com/mapbox-gl-js/example/cluster/ // Key Learnings Applied: // 1. Cluster configuration (cluster: true, clusterRadius: 50, clusterMaxZoom: 14) // 2. Step-based styling for clusters based on point count // 3. Click-to-expand cluster functionality with getClusterExpansionZoom() // 4. Performance optimization for large datasets (650 schools) // 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 }); // Configure globe atmosphere and space effects map.on('style.load', () => { map.setFog({ color: 'rgba(5, 10, 20, 0.9)', 'high-color': 'rgba(36, 92, 223, 0.35)', 'horizon-blend': 0.3, 'space-color': '#000814', 'star-intensity': 0.7 }); }); map.on('load', () => { // Add school data source with clustering enabled // Key clustering parameters from web research: // - cluster: true enables clustering // - clusterRadius: 50 defines aggregation radius // - clusterMaxZoom: 14 sets max zoom for clustering map.addSource('schools', { type: 'geojson', data: schoolData, cluster: true, clusterMaxZoom: 14, // Max zoom to cluster points on clusterRadius: 50, // Radius of each cluster when clustering points generateId: true // Performance optimization }); // Add cluster circle layer with step-based styling // Colors change based on cluster size (learned from web research): // - Blue for small clusters (<50 schools) // - Yellow for medium clusters (50-150 schools) // - Pink for large clusters (150+ schools) map.addLayer({ id: 'clusters', type: 'circle', source: 'schools', filter: ['has', 'point_count'], paint: { // Step expression for color based on point count 'circle-color': [ 'step', ['get', 'point_count'], '#51bbd6', // Blue for < 50 50, '#f1f075', // Yellow for 50-150 150, '#f28cb1' // Pink for > 150 ], // Step expression for radius based on point count 'circle-radius': [ 'step', ['get', 'point_count'], 20, // 20px for < 50 50, 30, // 30px for 50-150 150, 40 // 40px for > 150 ], 'circle-opacity': 0.85, 'circle-stroke-width': 2, 'circle-stroke-color': '#fff', 'circle-stroke-opacity': 0.6 } }); // Add cluster count labels map.addLayer({ id: 'cluster-count', type: 'symbol', source: 'schools', filter: ['has', 'point_count'], layout: { 'text-field': ['get', 'point_count_abbreviated'], 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'], 'text-size': 14 }, paint: { 'text-color': '#000000', 'text-halo-color': '#ffffff', 'text-halo-width': 1 } }); // Add unclustered point layer with color based on school type map.addLayer({ id: 'unclustered-point', type: 'circle', source: 'schools', filter: ['!', ['has', 'point_count']], paint: { 'circle-color': [ 'match', ['get', 'type'], 'Primary', '#667eea', 'Secondary', '#764ba2', 'University', '#f093fb', '#667eea' // Default ], 'circle-radius': [ 'interpolate', ['linear'], ['zoom'], 1, 3, 5, 5, 10, 8 ], 'circle-stroke-width': 2, 'circle-stroke-color': '#ffffff', 'circle-stroke-opacity': 0.8, 'circle-opacity': 0.9 } }); // Add resource indicator rings for individual schools map.addLayer({ id: 'resource-rings', type: 'circle', source: 'schools', filter: ['!', ['has', 'point_count']], paint: { 'circle-color': 'transparent', 'circle-radius': [ 'interpolate', ['linear'], ['zoom'], 1, 5, 5, 7, 10, 12 ], 'circle-stroke-width': 2, 'circle-stroke-color': [ 'step', ['get', 'resources'], '#f56565', // Red for < 50% resources 50, '#ecc94b', // Yellow for 50-80% 80, '#48bb78' // Green for > 80% ], 'circle-stroke-opacity': 0.7 } }); // Cluster click interaction - zoom to expansion level // Uses getClusterExpansionZoom() learned from web research map.on('click', 'clusters', (e) => { const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] }); const clusterId = features[0].properties.cluster_id; // Get the cluster expansion zoom level map.getSource('schools').getClusterExpansionZoom( clusterId, (err, zoom) => { if (err) return; // Smooth animation to expanded cluster map.easeTo({ center: features[0].geometry.coordinates, zoom: zoom + 0.5, duration: 800 }); } ); }); // Individual school popup on click map.on('click', 'unclustered-point', (e) => { const coordinates = e.features[0].geometry.coordinates.slice(); const props = e.features[0].properties; // Calculate resource status const resourceStatus = props.resources >= 80 ? 'Well Resourced' : props.resources >= 50 ? 'Moderately Resourced' : 'Under-resourced'; const resourceColor = props.resources >= 80 ? '#48bb78' : props.resources >= 50 ? '#ecc94b' : '#f56565'; // Ensure coordinates don't wrap around the globe while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) { coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360; } new mapboxgl.Popup() .setLngLat(coordinates) .setHTML(`
Type: ${props.type} School
Location: ${props.city}, ${props.country}
Students: ${props.students.toLocaleString()}
Teachers: ${props.teachers}
Student-Teacher Ratio: ${props.ratio}:1
Resources: ${props.resources}% (${resourceStatus})
Internet Access: ${props.internet ? '✓ Yes' : '✗ No'}
`) .addTo(map); }); // Change cursor on cluster hover map.on('mouseenter', 'clusters', () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', 'clusters', () => { map.getCanvas().style.cursor = ''; }); // Change cursor on school hover map.on('mouseenter', 'unclustered-point', () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', 'unclustered-point', () => { map.getCanvas().style.cursor = ''; }); // Gentle rotation animation let rotationSpeed = 0.15; let isUserInteracting = false; let lastInteractionTime = Date.now(); const rotateCamera = (timestamp) => { if (!isUserInteracting && Date.now() - lastInteractionTime > 2000) { map.rotateTo(map.getBearing() + rotationSpeed, { duration: 0 }); } requestAnimationFrame(rotateCamera); }; rotateCamera(); // Detect user interaction map.on('mousedown', () => { isUserInteracting = true; }); map.on('mouseup', () => { isUserInteracting = false; lastInteractionTime = Date.now(); }); map.on('dragend', () => { lastInteractionTime = Date.now(); }); map.on('zoomend', () => { lastInteractionTime = Date.now(); }); // Keyboard navigation for accessibility map.getCanvas().addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft') { map.rotateTo(map.getBearing() - 5); } else if (e.key === 'ArrowRight') { map.rotateTo(map.getBearing() + 5); } else if (e.key === 'ArrowUp') { map.zoomIn(); } else if (e.key === 'ArrowDown') { map.zoomOut(); } }); }); // Add navigation controls map.addControl(new mapboxgl.NavigationControl(), 'bottom-right'); // Add scale control map.addControl(new mapboxgl.ScaleControl({ maxWidth: 100, unit: 'metric' }), 'bottom-right'); // Add fullscreen control map.addControl(new mapboxgl.FullscreenControl(), 'bottom-right'); // Performance monitoring (console logging) map.on('load', () => { console.log('Mapbox Globe Iteration 8 - Educational Infrastructure Clustering'); console.log('Total schools loaded:', schoolData.features.length); console.log('Clustering enabled: radius 50px, max zoom 14'); console.log('Web research applied: Mapbox cluster API patterns'); console.log('Dataset: 311 educational facilities across 142 countries'); });