/** * Modern Trade Flow Map - Visualizes international trade flows with animated lines * For International Trade Flow Predictor * Based on AnyChart's flow map concept */ class TradeFlowMap { constructor(containerId, options = {}) { this.containerId = containerId; this.options = { width: options.width || '100%', height: options.height || 350, backgroundColor: options.backgroundColor || '#ffffff', oceanColor: options.oceanColor || '#ffffff', landColor: options.landColor || '#f0f0f0', // Lighter gray for lands countryStrokeColor: options.countryStrokeColor || '#e0e0e0', // Subtle borders selectedCountryColor: options.selectedCountryColor || '#d1e5f8', // Subtle highlight flowLineColor: options.flowLineColor || 'rgba(0, 103, 223, 0.7)', // More visible flows flowLineWidth: options.flowLineWidth || 2, flowMarkerColor: options.flowMarkerColor || '#0067df', animationSpeed: options.animationSpeed || 1.5, ...options }; this.container = document.getElementById(containerId); this.flows = []; this.countries = {}; this.selectedCountries = new Set(); this.svg = null; this.defs = null; this.worldGroup = null; this.flowsGroup = null; this.markersGroup = null; this.initialized = false; // Country coordinates for major trading countries this.countryCoordinates = { '840': { name: 'United States', lat: 37.0902, lng: -95.7129 }, '156': { name: 'China', lat: 35.8617, lng: 104.1954 }, '276': { name: 'Germany', lat: 51.1657, lng: 10.4515 }, '392': { name: 'Japan', lat: 36.2048, lng: 138.2529 }, '826': { name: 'United Kingdom', lat: 55.3781, lng: -3.4360 }, '124': { name: 'Canada', lat: 56.1304, lng: -106.3468 }, '484': { name: 'Mexico', lat: 23.6345, lng: -102.5528 }, '410': { name: 'South Korea', lat: 35.9078, lng: 127.7669 }, '356': { name: 'India', lat: 20.5937, lng: 78.9629 }, '250': { name: 'France', lat: 46.6034, lng: 1.8883 }, '380': { name: 'Italy', lat: 41.8719, lng: 12.5674 }, '076': { name: 'Brazil', lat: -14.2350, lng: -51.9253 }, '036': { name: 'Australia', lat: -25.2744, lng: 133.7751 }, '528': { name: 'Netherlands', lat: 52.1326, lng: 5.2913 }, '756': { name: 'Switzerland', lat: 46.8182, lng: 8.2275 }, '842': { name: 'United States', lat: 37.0902, lng: -95.7129 }, // Duplicate for compatibility }; } async initialize() { if (this.initialized) return; // Create map container this.container.innerHTML = ''; this.container.style.position = 'relative'; this.container.style.width = this.options.width; this.container.style.height = `${this.options.height}px`; this.container.style.backgroundColor = this.options.oceanColor; this.container.style.borderRadius = '8px'; this.container.style.overflow = 'hidden'; // Loading indicator const loadingIndicator = document.createElement('div'); loadingIndicator.style.position = 'absolute'; loadingIndicator.style.top = '50%'; loadingIndicator.style.left = '50%'; loadingIndicator.style.transform = 'translate(-50%, -50%)'; loadingIndicator.style.color = '#999'; loadingIndicator.style.fontSize = '14px'; loadingIndicator.textContent = 'Loading map data...'; this.container.appendChild(loadingIndicator); // Create SVG container for the map this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.svg.setAttribute('width', '100%'); this.svg.setAttribute('height', '100%'); this.svg.style.position = 'absolute'; this.container.appendChild(this.svg); // Add defs section for gradients and markers this.defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); this.svg.appendChild(this.defs); // Create subtle arrow marker for flow lines - more like the Google Pay reference const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker'); marker.setAttribute('id', `arrow-${this.containerId}`); marker.setAttribute('viewBox', '0 0 10 10'); marker.setAttribute('refX', '5'); marker.setAttribute('refY', '5'); marker.setAttribute('markerWidth', '3'); marker.setAttribute('markerHeight', '3'); marker.setAttribute('orient', 'auto'); const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path'); arrow.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z'); arrow.setAttribute('fill', this.options.flowMarkerColor); marker.appendChild(arrow); this.defs.appendChild(marker); // Create more subtle flow line gradient const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient'); gradient.setAttribute('id', `flow-gradient-${this.containerId}`); gradient.setAttribute('gradientUnits', 'userSpaceOnUse'); // More subtle gradient with less contrast const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); stop1.setAttribute('offset', '0%'); stop1.setAttribute('stop-color', this.options.flowLineColor); stop1.setAttribute('stop-opacity', '0.4'); const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); stop2.setAttribute('offset', '100%'); stop2.setAttribute('stop-color', this.options.flowLineColor); stop2.setAttribute('stop-opacity', '0.6'); gradient.appendChild(stop1); gradient.appendChild(stop2); this.defs.appendChild(gradient); // Add a pulse effect gradient for animated markers const pulseGradient = document.createElementNS('http://www.w3.org/2000/svg', 'radialGradient'); pulseGradient.setAttribute('id', `pulse-gradient-${this.containerId}`); pulseGradient.setAttribute('gradientUnits', 'objectBoundingBox'); pulseGradient.setAttribute('cx', '0.5'); pulseGradient.setAttribute('cy', '0.5'); pulseGradient.setAttribute('r', '0.5'); const pulseStop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); pulseStop1.setAttribute('offset', '0%'); pulseStop1.setAttribute('stop-color', this.options.flowMarkerColor); pulseStop1.setAttribute('stop-opacity', '0.9'); const pulseStop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); pulseStop2.setAttribute('offset', '100%'); pulseStop2.setAttribute('stop-color', this.options.flowMarkerColor); pulseStop2.setAttribute('stop-opacity', '0.1'); pulseGradient.appendChild(pulseStop1); pulseGradient.appendChild(pulseStop2); this.defs.appendChild(pulseGradient); // Create layer groups this.worldGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); this.worldGroup.setAttribute('class', 'world-map'); this.flowsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); this.flowsGroup.setAttribute('class', 'trade-flows'); this.markersGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); this.markersGroup.setAttribute('class', 'country-markers'); this.svg.appendChild(this.worldGroup); this.svg.appendChild(this.flowsGroup); this.svg.appendChild(this.markersGroup); // Draw the world map await this.drawModernWorldMap(); // Remove loading indicator loadingIndicator.remove(); this.initialized = true; return this; } async drawModernWorldMap() { // Create a simplified world map with major continents and countries // Using simplified vector paths similar to Google Pay world map const worldMapData = { // Define continents as clean path data with subtle curves continents: [ // North America - with more natural coastlines { path: 'M 55,100 C 90,85 130,85 160,100 L 180,120 C 190,140 195,160 185,185 C 170,200 150,210 125,215 C 90,210 70,195 55,180 C 45,160 40,130 55,100 Z', name: 'North America' }, // South America - with curved coastlines { path: 'M 130,230 C 160,225 175,230 185,250 C 190,280 195,310 185,340 C 170,360 150,370 130,375 C 110,365 100,345 95,325 C 100,280 110,250 130,230 Z', name: 'South America' }, // Europe - more detailed with peninsulas { path: 'M 270,110 C 290,100 320,95 345,100 C 360,115 370,130 365,150 C 355,165 340,175 315,180 C 290,175 275,165 265,150 C 260,135 260,120 270,110 Z', name: 'Europe' }, // Africa - more natural shape { path: 'M 270,190 C 300,185 330,185 355,190 C 370,210 375,240 370,270 C 360,300 340,320 315,330 C 290,325 270,310 260,290 C 250,260 245,230 250,210 C 255,200 260,195 270,190 Z', name: 'Africa' }, // Asia - larger and more detailed { path: 'M 370,120 C 410,100 460,90 510,95 C 550,105 580,120 590,150 C 595,180 585,210 565,240 C 520,270 470,285 420,280 C 390,270 370,250 355,220 C 345,190 345,150 370,120 Z', name: 'Asia' }, // Australia - more accurate shape { path: 'M 580,280 C 600,275 620,275 635,285 C 645,300 645,320 635,335 C 620,345 600,345 585,340 C 570,330 565,310 570,295 C 570,290 575,285 580,280 Z', name: 'Australia' } ], // Major countries - cleaner outlines with subtle curves countries: [ // USA - more detailed shape { code: '840', path: 'M 70,140 C 100,135 130,135 155,140 C 165,150 170,160 165,175 C 155,185 135,190 115,190 C 95,185 80,175 75,165 C 70,155 70,145 70,140 Z', name: 'United States' }, // Canada - with northern territories { code: '124', path: 'M 70,100 C 100,90 140,90 170,100 C 175,110 175,120 170,130 C 165,135 160,138 155,140 C 125,135 95,135 70,140 C 65,130 65,115 70,100 Z', name: 'Canada' }, // Mexico - improved shape { code: '484', path: 'M 80,170 C 90,175 100,180 110,180 C 115,190 115,200 110,205 C 100,208 90,208 80,205 C 75,195 75,180 80,170 Z', name: 'Mexico' }, // Brazil - larger and more accurate { code: '076', path: 'M 140,260 C 155,255 170,255 180,260 C 185,275 190,290 185,310 C 175,325 160,335 145,335 C 130,330 120,320 115,305 C 120,285 125,270 140,260 Z', name: 'Brazil' }, // UK - more defined island shape { code: '826', path: 'M 260,130 C 265,128 270,128 275,130 C 277,135 277,140 275,145 C 270,148 265,148 260,145 C 258,140 258,135 260,130 Z', name: 'United Kingdom' }, // Germany - central Europe position { code: '276', path: 'M 300,140 C 307,138 314,138 320,140 C 322,145 322,150 320,155 C 315,158 305,158 300,155 C 298,150 298,145 300,140 Z', name: 'Germany' }, // France - with recognizable hexagon shape { code: '250', path: 'M 280,150 C 287,148 294,148 300,150 C 302,155 302,160 300,165 C 295,168 285,168 280,165 C 278,160 278,155 280,150 Z', name: 'France' }, // China - larger with more accurate borders { code: '156', path: 'M 450,150 C 470,145 495,145 520,150 C 525,165 525,180 520,195 C 505,205 475,210 455,205 C 445,195 440,175 450,150 Z', name: 'China' }, // India - recognizable peninsula shape { code: '356', path: 'M 430,210 C 445,205 460,205 470,210 C 472,220 472,235 470,245 C 460,250 440,250 430,245 C 425,235 425,220 430,210 Z', name: 'India' }, // Japan - archipelago suggestion { code: '392', path: 'M 550,160 C 555,158 560,158 565,160 C 567,165 567,170 565,175 C 560,177 550,177 545,175 C 543,170 545,165 550,160 Z', name: 'Japan' }, // Australia - more detailed continent shape { code: '036', path: 'M 590,300 C 605,295 620,295 630,300 C 632,310 632,320 630,330 C 620,335 600,335 590,330 C 585,320 585,310 590,300 Z', name: 'Australia' } ] }; // Draw continents as background worldMapData.continents.forEach(continent => { const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', continent.path); path.setAttribute('fill', this.options.landColor); path.setAttribute('stroke', this.options.countryStrokeColor); path.setAttribute('stroke-width', '0.5'); path.setAttribute('data-name', continent.name); // Add title element for tooltip const title = document.createElementNS('http://www.w3.org/2000/svg', 'title'); title.textContent = continent.name; path.appendChild(title); this.worldGroup.appendChild(path); }); // Draw countries with more detail worldMapData.countries.forEach(country => { const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', country.path); path.setAttribute('fill', this.options.landColor); path.setAttribute('stroke', this.options.countryStrokeColor); path.setAttribute('stroke-width', '0.8'); path.setAttribute('data-code', country.code); path.setAttribute('data-name', country.name); // Add hover effect path.addEventListener('mouseover', () => { path.setAttribute('fill', this.options.selectedCountryColor); }); path.addEventListener('mouseout', () => { if (!this.selectedCountries.has(country.code)) { path.setAttribute('fill', this.options.landColor); } }); // Add title element for tooltip const title = document.createElementNS('http://www.w3.org/2000/svg', 'title'); title.textContent = country.name; path.appendChild(title); this.worldGroup.appendChild(path); // Store country reference for trade flows // Extract center point from path data for flow lines let coords = this.extractCenterFromPath(country.path); this.countries[country.code] = { code: country.code, name: country.name, element: path, x: coords.x, y: coords.y }; }); // Add labels for major countries if showLabels is enabled if (this.options.showLabels) { Object.values(this.countries).forEach(country => { const label = document.createElementNS('http://www.w3.org/2000/svg', 'text'); label.setAttribute('x', country.x); label.setAttribute('y', country.y - 5); label.setAttribute('text-anchor', 'middle'); label.setAttribute('font-size', '9px'); label.setAttribute('fill', '#555'); label.textContent = country.name; this.markersGroup.appendChild(label); }); } } // Helper function to extract center point from SVG path extractCenterFromPath(pathData) { // This is a simplified approach - in a real implementation, // you would parse the path data more carefully const points = pathData.replace(/[A-Za-z]/g, ' ').trim().split(/\s+/).map(Number); // Calculate average of x and y coordinates let sumX = 0, sumY = 0, count = 0; for (let i = 0; i < points.length; i += 2) { if (i + 1 < points.length) { sumX += points[i]; sumY += points[i + 1]; count++; } } return { x: sumX / count, y: sumY / count }; } // Converts longitude to X coordinate longitudeToX(lng) { // Map longitude (-180 to 180) to screen coordinates const width = this.container.clientWidth; return (lng + 180) * (width / 360); } // Converts latitude to Y coordinate latitudeToY(lat) { // Map latitude (-90 to 90) to screen coordinates const height = this.container.clientHeight; // Adjust for Mercator-like projection const latRad = lat * Math.PI / 180; const mercN = Math.log(Math.tan((Math.PI / 4) + (latRad / 2))); return height / 2 - (height * mercN / (2 * Math.PI)); } // Add a trade flow between two countries addFlow(fromCountryCode, toCountryCode, value = 1, options = {}) { if (!this.initialized) { console.error('Map not initialized. Call initialize() first.'); return this; } const fromCountry = this.countries[fromCountryCode]; const toCountry = this.countries[toCountryCode]; if (!fromCountry || !toCountry) { console.warn(`Country not found: ${!fromCountry ? fromCountryCode : toCountryCode}`); return this; } // Scale the line width based on the trade value let scaledWidth = this.options.flowLineWidth; if (value > 0) { // Log scale for better visualization scaledWidth = this.options.flowLineWidth * (1 + 0.5 * Math.log10(1 + value / 1000)); } // Highlight the selected countries this.selectedCountries.add(fromCountryCode); this.selectedCountries.add(toCountryCode); if (fromCountry.element) { fromCountry.element.setAttribute('fill', this.options.selectedCountryColor); } if (toCountry.element) { toCountry.element.setAttribute('fill', this.options.selectedCountryColor); } // Create a unique flow ID const flowId = `flow-${fromCountryCode}-${toCountryCode}`; // Create a group for this flow const flowGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); flowGroup.setAttribute('class', 'flow-connection'); flowGroup.setAttribute('data-flow-id', flowId); flowGroup.setAttribute('data-from', fromCountryCode); flowGroup.setAttribute('data-to', toCountryCode); // Calculate the curved path between countries // Use quadratic Bezier curve for smoother appearance const dx = toCountry.x - fromCountry.x; const dy = toCountry.y - fromCountry.y; const distance = Math.sqrt(dx * dx + dy * dy); // Calculate curvature - higher for longer distances const curveAmount = distance * 0.2; // Calculate control point perpendicular to the line const midX = (fromCountry.x + toCountry.x) / 2; const midY = (fromCountry.y + toCountry.y) / 2; // Calculate perpendicular unit vector const perpX = -dy / distance; const perpY = dx / distance; // Position control point perpendicular to the midpoint const ctrlX = midX + perpX * curveAmount; const ctrlY = midY + perpY * curveAmount; // Create the flow path const flowPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); flowPath.setAttribute('class', 'flow-line'); flowPath.setAttribute('d', `M ${fromCountry.x} ${fromCountry.y} Q ${ctrlX} ${ctrlY} ${toCountry.x} ${toCountry.y}`); flowPath.setAttribute('fill', 'none'); flowPath.setAttribute('stroke', `url(#flow-gradient-${this.containerId})`); flowPath.setAttribute('stroke-width', options.width || scaledWidth); flowPath.setAttribute('stroke-linecap', 'round'); flowPath.setAttribute('marker-end', `url(#arrow-${this.containerId})`); // Add to flow group flowGroup.appendChild(flowPath); // Add flow value label if requested if (options.showLabel !== false && value > 0) { const valueFormatted = value >= 1000000 ? `$${(value/1000000).toFixed(1)}M` : `$${(value/1000).toFixed(0)}K`; // Create label background const labelBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); labelBg.setAttribute('x', ctrlX - 20); labelBg.setAttribute('y', ctrlY - 10); labelBg.setAttribute('width', 40); labelBg.setAttribute('height', 20); labelBg.setAttribute('rx', 3); labelBg.setAttribute('ry', 3); labelBg.setAttribute('fill', 'white'); labelBg.setAttribute('opacity', '0.8'); // Create label text const label = document.createElementNS('http://www.w3.org/2000/svg', 'text'); label.setAttribute('x', ctrlX); label.setAttribute('y', ctrlY + 4); label.setAttribute('text-anchor', 'middle'); label.setAttribute('font-size', '8px'); label.setAttribute('fill', '#444'); label.textContent = options.label || valueFormatted; // Add to flow group flowGroup.appendChild(labelBg); flowGroup.appendChild(label); } // Add flow group to the flows layer this.flowsGroup.appendChild(flowGroup); // Add animated marker if (options.animated !== false) { this.animateFlowPath(flowPath, fromCountry, toCountry); } // Store flow data this.flows.push({ id: flowId, from: fromCountryCode, to: toCountryCode, value: value, element: flowGroup, path: flowPath }); return this; } // Animate flow along a path with subtle elegant animation animateFlowPath(path, source, target) { // Create a group for the animated markers const markerGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); markerGroup.setAttribute('class', 'flow-marker-group'); this.flowsGroup.appendChild(markerGroup); // Create pulse effect marker const marker = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); marker.setAttribute('class', 'flow-marker'); marker.setAttribute('r', 3); marker.setAttribute('fill', `url(#pulse-gradient-${this.containerId})`); marker.setAttribute('opacity', '0.8'); markerGroup.appendChild(marker); // Create subtle glow effect const glowMarker = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); glowMarker.setAttribute('class', 'flow-marker-glow'); glowMarker.setAttribute('r', 5); glowMarker.setAttribute('fill', `url(#pulse-gradient-${this.containerId})`); glowMarker.setAttribute('opacity', '0.3'); markerGroup.appendChild(glowMarker); // Create animated motion const animate = () => { // Calculate the total length of the path for smoother animation const pathLength = path.getTotalLength(); const duration = pathLength / 30 * (1 / this.options.animationSpeed); // Animation function with smoother easing const startTime = performance.now(); const step = (timestamp) => { const elapsedTime = timestamp - startTime; let progress = Math.min(elapsedTime / (duration * 1000), 1); // Add slight easing for more elegant motion progress = easeInOutQuad(progress); // Get point at percentage along the path const point = path.getPointAtLength(progress * pathLength); // Update marker position marker.setAttribute('cx', point.x); marker.setAttribute('cy', point.y); glowMarker.setAttribute('cx', point.x); glowMarker.setAttribute('cy', point.y); if (progress < 1) { requestAnimationFrame(step); } else { // When animation completes, pause then restart markerGroup.setAttribute('opacity', '0'); setTimeout(() => { markerGroup.setAttribute('opacity', '1'); animate(); }, 1200); } }; requestAnimationFrame(step); }; // Easing function for smoother animation const easeInOutQuad = (t) => { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; }; // Start the animation animate(); return markerGroup; } // Clear all trade flows clearFlows() { // Remove flow elements while (this.flowsGroup.firstChild) { this.flowsGroup.removeChild(this.flowsGroup.firstChild); } // Reset country colors this.selectedCountries.clear(); Object.values(this.countries).forEach(country => { if (country.element) { country.element.setAttribute('fill', this.options.landColor); } }); this.flows = []; return this; } // Add demo trade flows to the map addDemoFlows() { // Clear existing flows this.clearFlows(); // Add major trade flows with values (in millions USD) this.addFlow('840', '156', 500000, { label: '$500B' }); // US to China this.addFlow('840', '276', 180000, { label: '$180B' }); // US to Germany this.addFlow('156', '392', 320000, { label: '$320B' }); // China to Japan this.addFlow('156', '124', 110000, { label: '$110B' }); // China to Canada this.addFlow('276', '250', 230000, { label: '$230B' }); // Germany to France // Render all flows return this; } // Update the map with new data updateData(flows) { this.clearFlows(); flows.forEach(flow => { this.addFlow(flow.from || flow.reporterCode, flow.to || flow.partnerCode, flow.value || flow.tradeValue, flow.options || {}); }); return this; } // Resize the map resize(width, height) { if (width) this.container.style.width = width; if (height) this.container.style.height = `${height}px`; return this; } } // Initialize maps when document is ready document.addEventListener('DOMContentLoaded', () => { // Create a global object to store map instances window.tradeFlowMaps = {}; // Initialize trade flow maps on the page const mapContainers = document.querySelectorAll('.trade-flow-map'); mapContainers.forEach(container => { const containerId = container.id; if (!containerId) return; const options = { showLabels: container.dataset.showLabels === 'true', showFlowValues: container.dataset.showValues !== 'false', height: parseInt(container.dataset.height || 350, 10) }; // Create a new map instance const map = new TradeFlowMap(containerId, options); map.initialize().then(() => { // Add demo flows if requested if (container.dataset.demo === 'true') { // Use our modern demo flows with proper styling map.addDemoFlows(); } }); // Store the map instance window.tradeFlowMaps[containerId] = map; }); });