import { temperatureData } from './data/temperature-data.js'; // IMPORTANT: Replace with your Mapbox 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, 30], zoom: 1.5, pitch: 0 }); // State management let isRotating = true; let showDetailLayer = false; let userInteracting = false; let rotationInterval; // Auto-rotation function with smart pause function spinGlobe() { if (!userInteracting && isRotating) { const center = map.getCenter(); center.lng += 0.15; map.easeTo({ center, duration: 100, easing: (t) => t }); } } // Start rotation function startRotation() { if (!rotationInterval) { rotationInterval = setInterval(spinGlobe, 100); } } // Map load event map.on('load', () => { // Enable atmosphere (globe shell) map.setFog({ color: 'rgb(10, 20, 40)', 'high-color': 'rgb(5, 10, 25)', 'horizon-blend': 0.02, 'space-color': 'rgb(5, 8, 15)', 'star-intensity': 0.6 }); // Add temperature data source map.addSource('temperature', { type: 'geojson', data: temperatureData }); // HEATMAP LAYER - Primary visualization // Applying learnings from Mapbox heatmap documentation map.addLayer({ id: 'temperature-heatmap', type: 'heatmap', source: 'temperature', maxzoom: 15, paint: { // Heatmap weight based on temperature anomaly // Higher anomalies contribute more to the heatmap density 'heatmap-weight': [ 'interpolate', ['linear'], ['get', 'anomaly'], -1, 0.1, // Negative anomalies have low weight 0, 0.3, // Baseline has medium weight 1, 0.6, // +1°C anomaly 2, 0.8, // +2°C anomaly 3, 1.0 // +3°C and above have maximum weight ], // Heatmap intensity increases with zoom // This makes the heatmap more visible as you zoom in 'heatmap-intensity': [ 'interpolate', ['linear'], ['zoom'], 0, 0.8, // Lower intensity at global view 5, 1.2, // Medium intensity at regional view 10, 1.5 // Higher intensity at closer zoom ], // Heatmap color gradient - diverging scheme for temperature // Blue for cooling, yellow/orange/red for warming 'heatmap-color': [ 'interpolate', ['linear'], ['heatmap-density'], 0, 'rgba(0, 0, 0, 0)', // Transparent at zero density 0.15, 'rgba(0, 102, 255, 0.4)', // Blue for cooler areas 0.25, 'rgba(0, 204, 255, 0.5)', // Cyan 0.35, 'rgba(0, 255, 136, 0.6)', // Green-cyan (neutral) 0.5, 'rgba(255, 255, 0, 0.7)', // Yellow (moderate warming) 0.65, 'rgba(255, 136, 0, 0.8)', // Orange (significant warming) 0.8, 'rgba(255, 0, 0, 0.9)', // Red (high warming) 1.0, 'rgba(204, 0, 0, 1.0)' // Dark red (extreme warming) ], // Heatmap radius controls the spread of each point's influence // Larger radius at lower zoom for better visibility 'heatmap-radius': [ 'interpolate', ['linear'], ['zoom'], 0, 15, // Large radius at global view 5, 25, // Medium radius at regional view 10, 35 // Smaller radius at closer zoom ], // Heatmap opacity decreases at higher zoom to transition to circles 'heatmap-opacity': [ 'interpolate', ['linear'], ['zoom'], 0, 0.9, // Full opacity at global view 7, 0.8, // Start fading 10, 0.3, // Mostly transparent 15, 0 // Invisible at high zoom (circles take over) ] } }); // CIRCLE LAYER - Detail view at higher zoom levels // This complements the heatmap by showing individual stations map.addLayer({ id: 'temperature-circles', type: 'circle', source: 'temperature', minzoom: 7, paint: { // Circle radius based on anomaly magnitude 'circle-radius': [ 'interpolate', ['linear'], ['zoom'], 7, [ 'interpolate', ['linear'], ['abs', ['get', 'anomaly']], 0, 3, 1, 5, 2, 8, 3, 12 ], 12, [ 'interpolate', ['linear'], ['abs', ['get', 'anomaly']], 0, 5, 1, 8, 2, 12, 3, 18 ] ], // Circle color based on anomaly value 'circle-color': [ 'interpolate', ['linear'], ['get', 'anomaly'], -1.0, '#0066ff', // Blue (cooling) -0.5, '#00ccff', // Cyan 0.0, '#00ff88', // Green (neutral) 0.5, '#ffff00', // Yellow 1.0, '#ffaa00', // Orange 1.5, '#ff6600', // Orange-red 2.0, '#ff0000', // Red 2.5, '#dd0000', // Dark red 3.0, '#cc0000' // Darkest red ], // Circle opacity increases with zoom 'circle-opacity': [ 'interpolate', ['linear'], ['zoom'], 7, 0, // Start invisible 10, 0.6, // Fade in 15, 0.85 // Full visibility ], // Circle stroke for better visibility 'circle-stroke-width': [ 'interpolate', ['linear'], ['zoom'], 7, 0.5, 12, 1.5 ], 'circle-stroke-color': '#ffffff', 'circle-stroke-opacity': 0.6 } }); // Start auto-rotation startRotation(); // Popup for detailed information const popup = new mapboxgl.Popup({ closeButton: false, closeOnClick: false, offset: 15 }); // Mouse events for circles layer map.on('mouseenter', 'temperature-circles', (e) => { map.getCanvas().style.cursor = 'pointer'; const coordinates = e.features[0].geometry.coordinates.slice(); const props = e.features[0].properties; const html = `

${props.station}

Temperature: ${props.temperature.toFixed(1)}°C
Anomaly: ${props.anomaly > 0 ? '+' : ''}${props.anomaly.toFixed(1)}°C
Baseline: ${props.baseline.toFixed(1)}°C
Year: ${props.year}
`; popup.setLngLat(coordinates).setHTML(html).addTo(map); }); map.on('mouseleave', 'temperature-circles', () => { map.getCanvas().style.cursor = ''; popup.remove(); }); }); // User interaction detection for smart rotation pause map.on('mousedown', () => { userInteracting = true; }); map.on('mouseup', () => { userInteracting = false; }); map.on('dragstart', () => { userInteracting = true; }); map.on('dragend', () => { userInteracting = false; }); map.on('touchstart', () => { userInteracting = true; }); map.on('touchend', () => { userInteracting = false; }); // Controls document.getElementById('toggle-rotation').addEventListener('click', function() { isRotating = !isRotating; this.textContent = isRotating ? 'Auto-Rotate: ON' : 'Auto-Rotate: OFF'; this.classList.toggle('active'); if (!isRotating && rotationInterval) { clearInterval(rotationInterval); rotationInterval = null; } else if (isRotating) { startRotation(); } }); document.getElementById('toggle-layer').addEventListener('click', function() { showDetailLayer = !showDetailLayer; if (showDetailLayer) { // Zoom in to show circles better map.flyTo({ zoom: 3.5, duration: 1500 }); this.textContent = 'Show Heatmap View'; } else { // Zoom out to show global heatmap map.flyTo({ zoom: 1.5, duration: 1500 }); this.textContent = 'Show Detail View'; } }); document.getElementById('reset-view').addEventListener('click', function() { map.flyTo({ center: [20, 30], zoom: 1.5, pitch: 0, bearing: 0, duration: 1500 }); // Reset rotation if it was off if (!isRotating) { isRotating = true; document.getElementById('toggle-rotation').textContent = 'Auto-Rotate: ON'; document.getElementById('toggle-rotation').classList.add('active'); startRotation(); } }); // Add navigation controls map.addControl(new mapboxgl.NavigationControl(), 'bottom-right'); map.addControl(new mapboxgl.FullscreenControl(), 'bottom-right');