371 lines
9.8 KiB
JavaScript
371 lines
9.8 KiB
JavaScript
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 = `
|
|
<div style="font-family: Arial, sans-serif; min-width: 220px;">
|
|
<h3 style="margin: 0 0 10px 0; font-size: 16px; font-weight: bold; color: #333;">
|
|
${props.country}
|
|
</h3>
|
|
<div style="font-size: 13px; line-height: 1.6; color: #555;">
|
|
<div style="margin-bottom: 6px;">
|
|
<strong>GDP per Capita:</strong> $${props.gdpPerCapita.toLocaleString()}
|
|
</div>
|
|
<div style="margin-bottom: 6px; color: ${props.growthRate >= 0 ? '#2166ac' : '#b2182b'};">
|
|
<strong>Growth Rate:</strong> ${props.growthRate >= 0 ? '+' : ''}${props.growthRate}%
|
|
</div>
|
|
<div style="margin-bottom: 6px;">
|
|
<strong>Development Index:</strong> ${(props.developmentIndex || props.developmentRate || 0).toFixed(3)}
|
|
</div>
|
|
<div>
|
|
<strong>Trade Volume:</strong> $${props.tradeVolume}B
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div style="font-weight: bold; margin-bottom: 8px; font-size: 13px;">
|
|
Size: ${sizeLabels[activeMetric]}
|
|
</div>
|
|
<div style="display: flex; align-items: flex-end; gap: 8px;">
|
|
<div style="text-align: center;">
|
|
<div style="width: ${sizeScale.min * 2}px; height: ${sizeScale.min * 2}px; border-radius: 50%; background: rgba(100,149,237,0.6); margin: 0 auto 4px;"></div>
|
|
<div style="font-size: 11px;">${formatValue(sizeScale.stops[0][0], activeMetric)}</div>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<div style="width: ${sizeScale.max * 2}px; height: ${sizeScale.max * 2}px; border-radius: 50%; background: rgba(100,149,237,0.6); margin: 0 auto 4px;"></div>
|
|
<div style="font-size: 11px;">${formatValue(sizeScale.stops[sizeScale.stops.length - 1][0], activeMetric)}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div style="font-weight: bold; margin-bottom: 8px; font-size: 13px;">
|
|
Color: ${colorLabels[activeColorMetric]}
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<div style="font-size: 11px;">${formatValue(colorScale.stops[0][0], activeColorMetric)}</div>
|
|
<div style="flex: 1; height: 20px; background: linear-gradient(to right, ${gradientStops}); border-radius: 3px;"></div>
|
|
<div style="font-size: 11px;">${formatValue(colorScale.stops[colorScale.stops.length - 1][0], activeColorMetric)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 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 });
|
|
}
|
|
});
|