import { economicData } from './data/economic-data.js'; // 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.5, attributionControl: false }); // Add attribution and navigation controls map.addControl(new mapboxgl.AttributionControl({ compact: true }), 'bottom-right'); map.addControl(new mapboxgl.NavigationControl(), 'top-right'); // Configuration for atmosphere map.on('style.load', () => { map.setFog({ color: 'rgb(186, 210, 235)', 'high-color': 'rgb(36, 92, 223)', 'horizon-blend': 0.02, 'space-color': 'rgb(11, 11, 25)', 'star-intensity': 0.6 }); }); // Active metric for visualization let activeMetric = 'gdpPerCapita'; let activeColorMetric = 'growthRate'; // Color scales and data ranges const colorScales = { growthRate: { // Diverging scale: negative (red) to zero (white) to positive (green) stops: [ [-25, '#b2182b'], // Deep red for severe contraction [-10, '#ef8a62'], // Light red for contraction [-5, '#fddbc7'], // Very light red [0, '#f7f7f7'], // White for zero growth [2, '#d1e5f0'], // Very light blue [4, '#67a9cf'], // Light blue for moderate growth [8, '#2166ac'] // Deep blue for strong growth ] }, developmentIndex: { // Sequential scale for development (low to high) stops: [ [0.35, '#fff7ec'], [0.45, '#fee8c8'], [0.55, '#fdd49e'], [0.65, '#fdbb84'], [0.75, '#fc8d59'], [0.85, '#ef6548'], [0.95, '#d7301f'] ] }, gdpPerCapita: { // Sequential scale for GDP (low to high) stops: [ [0, '#f7fcf5'], [5000, '#e5f5e0'], [10000, '#c7e9c0'], [20000, '#a1d99b'], [40000, '#74c476'], [60000, '#41ab5d'], [100000, '#238b45'] ] }, tradeVolume: { // Sequential scale for trade stops: [ [0, '#fff5f0'], [50, '#fee0d2'], [200, '#fcbba1'], [500, '#fc9272'], [1000, '#fb6a4a'], [3000, '#ef3b2c'], [6000, '#a50f15'] ] } }; // Size scales const sizeScales = { gdpPerCapita: { min: 3, max: 25, stops: [ [0, 3], [10000, 8], [30000, 15], [70000, 25] ] }, tradeVolume: { min: 3, max: 30, stops: [ [0, 3], [100, 8], [500, 15], [2000, 22], [6000, 30] ] }, developmentIndex: { min: 4, max: 20, stops: [ [0.35, 4], [0.55, 8], [0.75, 14], [0.95, 20] ] }, growthRate: { min: 5, max: 20, stops: [ [-25, 5], [-5, 8], [0, 10], [5, 15], [12, 20] ] } }; // Add economic data layer map.on('load', () => { // Add source map.addSource('economic-indicators', { type: 'geojson', data: economicData }); // Add circle layer with data-driven styling map.addLayer({ id: 'economic-circles', type: 'circle', source: 'economic-indicators', paint: { // Circle radius based on active size metric using interpolate expression 'circle-radius': [ 'interpolate', ['linear'], ['get', activeMetric], ...sizeScales[activeMetric].stops.flat() ], // Circle color based on growth rate using interpolate expression 'circle-color': [ 'interpolate', ['linear'], ['get', activeColorMetric], ...colorScales[activeColorMetric].stops.flat() ], // Opacity with zoom-based adjustment 'circle-opacity': [ 'interpolate', ['linear'], ['zoom'], 1, 0.7, 4, 0.8, 8, 0.9 ], // Stroke for better visibility 'circle-stroke-width': [ 'interpolate', ['linear'], ['zoom'], 1, 0.5, 4, 1, 8, 2 ], 'circle-stroke-color': '#ffffff', 'circle-stroke-opacity': 0.5 } }); // Add country labels layer map.addLayer({ id: 'country-labels', type: 'symbol', source: 'economic-indicators', layout: { 'text-field': ['get', 'code'], 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'], 'text-size': [ 'interpolate', ['linear'], ['zoom'], 1, 8, 4, 12, 8, 16 ], 'text-offset': [0, 0], 'text-anchor': 'center' }, paint: { 'text-color': '#ffffff', 'text-halo-color': '#000000', 'text-halo-width': 1, 'text-opacity': [ 'interpolate', ['linear'], ['zoom'], 1, 0, 2, 0.6, 4, 1 ] }, minzoom: 1.5 }); // Create popup const popup = new mapboxgl.Popup({ closeButton: false, closeOnClick: false, offset: 10 }); // Show popup on hover map.on('mouseenter', 'economic-circles', (e) => { map.getCanvas().style.cursor = 'pointer'; const coordinates = e.features[0].geometry.coordinates.slice(); const props = e.features[0].properties; const html = `

${props.country}

GDP per Capita: $${props.gdpPerCapita.toLocaleString()}
Growth Rate: ${props.growthRate >= 0 ? '+' : ''}${props.growthRate}%
Development Index: ${(props.developmentIndex || props.developmentRate || 0).toFixed(3)}
Trade Volume: $${props.tradeVolume}B
`; popup.setLngLat(coordinates).setHTML(html).addTo(map); }); map.on('mouseleave', 'economic-circles', () => { map.getCanvas().style.cursor = ''; popup.remove(); }); // Update visualization function window.updateVisualization = (sizeMetric, colorMetric) => { activeMetric = sizeMetric; activeColorMetric = colorMetric; // Update circle radius map.setPaintProperty('economic-circles', 'circle-radius', [ 'interpolate', ['linear'], ['get', activeMetric], ...sizeScales[activeMetric].stops.flat() ]); // Update circle color map.setPaintProperty('economic-circles', 'circle-color', [ 'interpolate', ['linear'], ['get', activeColorMetric], ...colorScales[activeColorMetric].stops.flat() ]); // Update legend updateLegend(); }; // Initialize legend updateLegend(); }); // Update legend based on active metrics function updateLegend() { const sizeLegend = document.getElementById('size-legend'); const colorLegend = document.getElementById('color-legend'); // Size legend const sizeScale = sizeScales[activeMetric]; const sizeLabels = { gdpPerCapita: 'GDP per Capita ($)', tradeVolume: 'Trade Volume ($B)', developmentIndex: 'Development Index', growthRate: 'Growth Rate (%)' }; sizeLegend.innerHTML = `
Size: ${sizeLabels[activeMetric]}
${formatValue(sizeScale.stops[0][0], activeMetric)}
${formatValue(sizeScale.stops[sizeScale.stops.length - 1][0], activeMetric)}
`; // Color legend const colorScale = colorScales[activeColorMetric]; const colorLabels = { gdpPerCapita: 'GDP per Capita ($)', tradeVolume: 'Trade Volume ($B)', developmentIndex: 'Development Index', growthRate: 'Growth Rate (%)' }; const gradientStops = colorScale.stops.map((stop, i) => { const percent = (i / (colorScale.stops.length - 1)) * 100; return `${stop[1]} ${percent}%`; }).join(', '); colorLegend.innerHTML = `
Color: ${colorLabels[activeColorMetric]}
${formatValue(colorScale.stops[0][0], activeColorMetric)}
${formatValue(colorScale.stops[colorScale.stops.length - 1][0], activeColorMetric)}
`; } // Format values based on metric type function formatValue(value, metric) { if (metric === 'gdpPerCapita') { return `$${(value / 1000).toFixed(0)}k`; } else if (metric === 'tradeVolume') { return `$${value}B`; } else if (metric === 'developmentIndex') { return value.toFixed(2); } else if (metric === 'growthRate') { return value >= 0 ? `+${value}%` : `${value}%`; } return value; } // Enable rotation map.on('idle', () => { if (map.getLayer('economic-circles')) { map.rotateTo((map.getBearing() + 0.3) % 360, { duration: 120000 }); } });