Spaces:
Sleeping
Sleeping
Upload static/js/tradeflowmap.js with huggingface_hub
Browse files- static/js/tradeflowmap.js +682 -0
static/js/tradeflowmap.js
ADDED
@@ -0,0 +1,682 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Modern Trade Flow Map - Visualizes international trade flows with animated lines
|
3 |
+
* For International Trade Flow Predictor
|
4 |
+
* Based on AnyChart's flow map concept
|
5 |
+
*/
|
6 |
+
|
7 |
+
class TradeFlowMap {
|
8 |
+
constructor(containerId, options = {}) {
|
9 |
+
this.containerId = containerId;
|
10 |
+
this.options = {
|
11 |
+
width: options.width || '100%',
|
12 |
+
height: options.height || 350,
|
13 |
+
backgroundColor: options.backgroundColor || '#ffffff',
|
14 |
+
oceanColor: options.oceanColor || '#ffffff',
|
15 |
+
landColor: options.landColor || '#f0f0f0', // Lighter gray for lands
|
16 |
+
countryStrokeColor: options.countryStrokeColor || '#e0e0e0', // Subtle borders
|
17 |
+
selectedCountryColor: options.selectedCountryColor || '#d1e5f8', // Subtle highlight
|
18 |
+
flowLineColor: options.flowLineColor || 'rgba(0, 103, 223, 0.7)', // More visible flows
|
19 |
+
flowLineWidth: options.flowLineWidth || 2,
|
20 |
+
flowMarkerColor: options.flowMarkerColor || '#0067df',
|
21 |
+
animationSpeed: options.animationSpeed || 1.5,
|
22 |
+
...options
|
23 |
+
};
|
24 |
+
|
25 |
+
this.container = document.getElementById(containerId);
|
26 |
+
this.flows = [];
|
27 |
+
this.countries = {};
|
28 |
+
this.selectedCountries = new Set();
|
29 |
+
this.svg = null;
|
30 |
+
this.defs = null;
|
31 |
+
this.worldGroup = null;
|
32 |
+
this.flowsGroup = null;
|
33 |
+
this.markersGroup = null;
|
34 |
+
this.initialized = false;
|
35 |
+
|
36 |
+
// Country coordinates for major trading countries
|
37 |
+
this.countryCoordinates = {
|
38 |
+
'840': { name: 'United States', lat: 37.0902, lng: -95.7129 },
|
39 |
+
'156': { name: 'China', lat: 35.8617, lng: 104.1954 },
|
40 |
+
'276': { name: 'Germany', lat: 51.1657, lng: 10.4515 },
|
41 |
+
'392': { name: 'Japan', lat: 36.2048, lng: 138.2529 },
|
42 |
+
'826': { name: 'United Kingdom', lat: 55.3781, lng: -3.4360 },
|
43 |
+
'124': { name: 'Canada', lat: 56.1304, lng: -106.3468 },
|
44 |
+
'484': { name: 'Mexico', lat: 23.6345, lng: -102.5528 },
|
45 |
+
'410': { name: 'South Korea', lat: 35.9078, lng: 127.7669 },
|
46 |
+
'356': { name: 'India', lat: 20.5937, lng: 78.9629 },
|
47 |
+
'250': { name: 'France', lat: 46.6034, lng: 1.8883 },
|
48 |
+
'380': { name: 'Italy', lat: 41.8719, lng: 12.5674 },
|
49 |
+
'076': { name: 'Brazil', lat: -14.2350, lng: -51.9253 },
|
50 |
+
'036': { name: 'Australia', lat: -25.2744, lng: 133.7751 },
|
51 |
+
'528': { name: 'Netherlands', lat: 52.1326, lng: 5.2913 },
|
52 |
+
'756': { name: 'Switzerland', lat: 46.8182, lng: 8.2275 },
|
53 |
+
'842': { name: 'United States', lat: 37.0902, lng: -95.7129 }, // Duplicate for compatibility
|
54 |
+
};
|
55 |
+
}
|
56 |
+
|
57 |
+
async initialize() {
|
58 |
+
if (this.initialized) return;
|
59 |
+
|
60 |
+
// Create map container
|
61 |
+
this.container.innerHTML = '';
|
62 |
+
this.container.style.position = 'relative';
|
63 |
+
this.container.style.width = this.options.width;
|
64 |
+
this.container.style.height = `${this.options.height}px`;
|
65 |
+
this.container.style.backgroundColor = this.options.oceanColor;
|
66 |
+
this.container.style.borderRadius = '8px';
|
67 |
+
this.container.style.overflow = 'hidden';
|
68 |
+
|
69 |
+
// Loading indicator
|
70 |
+
const loadingIndicator = document.createElement('div');
|
71 |
+
loadingIndicator.style.position = 'absolute';
|
72 |
+
loadingIndicator.style.top = '50%';
|
73 |
+
loadingIndicator.style.left = '50%';
|
74 |
+
loadingIndicator.style.transform = 'translate(-50%, -50%)';
|
75 |
+
loadingIndicator.style.color = '#999';
|
76 |
+
loadingIndicator.style.fontSize = '14px';
|
77 |
+
loadingIndicator.textContent = 'Loading map data...';
|
78 |
+
this.container.appendChild(loadingIndicator);
|
79 |
+
|
80 |
+
// Create SVG container for the map
|
81 |
+
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
82 |
+
this.svg.setAttribute('width', '100%');
|
83 |
+
this.svg.setAttribute('height', '100%');
|
84 |
+
this.svg.style.position = 'absolute';
|
85 |
+
this.container.appendChild(this.svg);
|
86 |
+
|
87 |
+
// Add defs section for gradients and markers
|
88 |
+
this.defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
89 |
+
this.svg.appendChild(this.defs);
|
90 |
+
|
91 |
+
// Create subtle arrow marker for flow lines - more like the Google Pay reference
|
92 |
+
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
93 |
+
marker.setAttribute('id', `arrow-${this.containerId}`);
|
94 |
+
marker.setAttribute('viewBox', '0 0 10 10');
|
95 |
+
marker.setAttribute('refX', '5');
|
96 |
+
marker.setAttribute('refY', '5');
|
97 |
+
marker.setAttribute('markerWidth', '3');
|
98 |
+
marker.setAttribute('markerHeight', '3');
|
99 |
+
marker.setAttribute('orient', 'auto');
|
100 |
+
|
101 |
+
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
102 |
+
arrow.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
|
103 |
+
arrow.setAttribute('fill', this.options.flowMarkerColor);
|
104 |
+
marker.appendChild(arrow);
|
105 |
+
this.defs.appendChild(marker);
|
106 |
+
|
107 |
+
// Create more subtle flow line gradient
|
108 |
+
const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
|
109 |
+
gradient.setAttribute('id', `flow-gradient-${this.containerId}`);
|
110 |
+
gradient.setAttribute('gradientUnits', 'userSpaceOnUse');
|
111 |
+
|
112 |
+
// More subtle gradient with less contrast
|
113 |
+
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
114 |
+
stop1.setAttribute('offset', '0%');
|
115 |
+
stop1.setAttribute('stop-color', this.options.flowLineColor);
|
116 |
+
stop1.setAttribute('stop-opacity', '0.4');
|
117 |
+
|
118 |
+
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
119 |
+
stop2.setAttribute('offset', '100%');
|
120 |
+
stop2.setAttribute('stop-color', this.options.flowLineColor);
|
121 |
+
stop2.setAttribute('stop-opacity', '0.6');
|
122 |
+
|
123 |
+
gradient.appendChild(stop1);
|
124 |
+
gradient.appendChild(stop2);
|
125 |
+
this.defs.appendChild(gradient);
|
126 |
+
|
127 |
+
// Add a pulse effect gradient for animated markers
|
128 |
+
const pulseGradient = document.createElementNS('http://www.w3.org/2000/svg', 'radialGradient');
|
129 |
+
pulseGradient.setAttribute('id', `pulse-gradient-${this.containerId}`);
|
130 |
+
pulseGradient.setAttribute('gradientUnits', 'objectBoundingBox');
|
131 |
+
pulseGradient.setAttribute('cx', '0.5');
|
132 |
+
pulseGradient.setAttribute('cy', '0.5');
|
133 |
+
pulseGradient.setAttribute('r', '0.5');
|
134 |
+
|
135 |
+
const pulseStop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
136 |
+
pulseStop1.setAttribute('offset', '0%');
|
137 |
+
pulseStop1.setAttribute('stop-color', this.options.flowMarkerColor);
|
138 |
+
pulseStop1.setAttribute('stop-opacity', '0.9');
|
139 |
+
|
140 |
+
const pulseStop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
|
141 |
+
pulseStop2.setAttribute('offset', '100%');
|
142 |
+
pulseStop2.setAttribute('stop-color', this.options.flowMarkerColor);
|
143 |
+
pulseStop2.setAttribute('stop-opacity', '0.1');
|
144 |
+
|
145 |
+
pulseGradient.appendChild(pulseStop1);
|
146 |
+
pulseGradient.appendChild(pulseStop2);
|
147 |
+
this.defs.appendChild(pulseGradient);
|
148 |
+
|
149 |
+
// Create layer groups
|
150 |
+
this.worldGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
151 |
+
this.worldGroup.setAttribute('class', 'world-map');
|
152 |
+
|
153 |
+
this.flowsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
154 |
+
this.flowsGroup.setAttribute('class', 'trade-flows');
|
155 |
+
|
156 |
+
this.markersGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
157 |
+
this.markersGroup.setAttribute('class', 'country-markers');
|
158 |
+
|
159 |
+
this.svg.appendChild(this.worldGroup);
|
160 |
+
this.svg.appendChild(this.flowsGroup);
|
161 |
+
this.svg.appendChild(this.markersGroup);
|
162 |
+
|
163 |
+
// Draw the world map
|
164 |
+
await this.drawModernWorldMap();
|
165 |
+
|
166 |
+
// Remove loading indicator
|
167 |
+
loadingIndicator.remove();
|
168 |
+
|
169 |
+
this.initialized = true;
|
170 |
+
return this;
|
171 |
+
}
|
172 |
+
|
173 |
+
async drawModernWorldMap() {
|
174 |
+
// Create a simplified world map with major continents and countries
|
175 |
+
// Using simplified vector paths similar to Google Pay world map
|
176 |
+
const worldMapData = {
|
177 |
+
// Define continents as clean path data with subtle curves
|
178 |
+
continents: [
|
179 |
+
// North America - with more natural coastlines
|
180 |
+
{
|
181 |
+
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',
|
182 |
+
name: 'North America'
|
183 |
+
},
|
184 |
+
// South America - with curved coastlines
|
185 |
+
{
|
186 |
+
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',
|
187 |
+
name: 'South America'
|
188 |
+
},
|
189 |
+
// Europe - more detailed with peninsulas
|
190 |
+
{
|
191 |
+
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',
|
192 |
+
name: 'Europe'
|
193 |
+
},
|
194 |
+
// Africa - more natural shape
|
195 |
+
{
|
196 |
+
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',
|
197 |
+
name: 'Africa'
|
198 |
+
},
|
199 |
+
// Asia - larger and more detailed
|
200 |
+
{
|
201 |
+
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',
|
202 |
+
name: 'Asia'
|
203 |
+
},
|
204 |
+
// Australia - more accurate shape
|
205 |
+
{
|
206 |
+
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',
|
207 |
+
name: 'Australia'
|
208 |
+
}
|
209 |
+
],
|
210 |
+
|
211 |
+
// Major countries - cleaner outlines with subtle curves
|
212 |
+
countries: [
|
213 |
+
// USA - more detailed shape
|
214 |
+
{
|
215 |
+
code: '840',
|
216 |
+
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',
|
217 |
+
name: 'United States'
|
218 |
+
},
|
219 |
+
// Canada - with northern territories
|
220 |
+
{
|
221 |
+
code: '124',
|
222 |
+
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',
|
223 |
+
name: 'Canada'
|
224 |
+
},
|
225 |
+
// Mexico - improved shape
|
226 |
+
{
|
227 |
+
code: '484',
|
228 |
+
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',
|
229 |
+
name: 'Mexico'
|
230 |
+
},
|
231 |
+
// Brazil - larger and more accurate
|
232 |
+
{
|
233 |
+
code: '076',
|
234 |
+
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',
|
235 |
+
name: 'Brazil'
|
236 |
+
},
|
237 |
+
// UK - more defined island shape
|
238 |
+
{
|
239 |
+
code: '826',
|
240 |
+
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',
|
241 |
+
name: 'United Kingdom'
|
242 |
+
},
|
243 |
+
// Germany - central Europe position
|
244 |
+
{
|
245 |
+
code: '276',
|
246 |
+
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',
|
247 |
+
name: 'Germany'
|
248 |
+
},
|
249 |
+
// France - with recognizable hexagon shape
|
250 |
+
{
|
251 |
+
code: '250',
|
252 |
+
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',
|
253 |
+
name: 'France'
|
254 |
+
},
|
255 |
+
// China - larger with more accurate borders
|
256 |
+
{
|
257 |
+
code: '156',
|
258 |
+
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',
|
259 |
+
name: 'China'
|
260 |
+
},
|
261 |
+
// India - recognizable peninsula shape
|
262 |
+
{
|
263 |
+
code: '356',
|
264 |
+
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',
|
265 |
+
name: 'India'
|
266 |
+
},
|
267 |
+
// Japan - archipelago suggestion
|
268 |
+
{
|
269 |
+
code: '392',
|
270 |
+
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',
|
271 |
+
name: 'Japan'
|
272 |
+
},
|
273 |
+
// Australia - more detailed continent shape
|
274 |
+
{
|
275 |
+
code: '036',
|
276 |
+
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',
|
277 |
+
name: 'Australia'
|
278 |
+
}
|
279 |
+
]
|
280 |
+
};
|
281 |
+
|
282 |
+
// Draw continents as background
|
283 |
+
worldMapData.continents.forEach(continent => {
|
284 |
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
285 |
+
path.setAttribute('d', continent.path);
|
286 |
+
path.setAttribute('fill', this.options.landColor);
|
287 |
+
path.setAttribute('stroke', this.options.countryStrokeColor);
|
288 |
+
path.setAttribute('stroke-width', '0.5');
|
289 |
+
path.setAttribute('data-name', continent.name);
|
290 |
+
|
291 |
+
// Add title element for tooltip
|
292 |
+
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
293 |
+
title.textContent = continent.name;
|
294 |
+
path.appendChild(title);
|
295 |
+
|
296 |
+
this.worldGroup.appendChild(path);
|
297 |
+
});
|
298 |
+
|
299 |
+
// Draw countries with more detail
|
300 |
+
worldMapData.countries.forEach(country => {
|
301 |
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
302 |
+
path.setAttribute('d', country.path);
|
303 |
+
path.setAttribute('fill', this.options.landColor);
|
304 |
+
path.setAttribute('stroke', this.options.countryStrokeColor);
|
305 |
+
path.setAttribute('stroke-width', '0.8');
|
306 |
+
path.setAttribute('data-code', country.code);
|
307 |
+
path.setAttribute('data-name', country.name);
|
308 |
+
|
309 |
+
// Add hover effect
|
310 |
+
path.addEventListener('mouseover', () => {
|
311 |
+
path.setAttribute('fill', this.options.selectedCountryColor);
|
312 |
+
});
|
313 |
+
|
314 |
+
path.addEventListener('mouseout', () => {
|
315 |
+
if (!this.selectedCountries.has(country.code)) {
|
316 |
+
path.setAttribute('fill', this.options.landColor);
|
317 |
+
}
|
318 |
+
});
|
319 |
+
|
320 |
+
// Add title element for tooltip
|
321 |
+
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
322 |
+
title.textContent = country.name;
|
323 |
+
path.appendChild(title);
|
324 |
+
|
325 |
+
this.worldGroup.appendChild(path);
|
326 |
+
|
327 |
+
// Store country reference for trade flows
|
328 |
+
// Extract center point from path data for flow lines
|
329 |
+
let coords = this.extractCenterFromPath(country.path);
|
330 |
+
this.countries[country.code] = {
|
331 |
+
code: country.code,
|
332 |
+
name: country.name,
|
333 |
+
element: path,
|
334 |
+
x: coords.x,
|
335 |
+
y: coords.y
|
336 |
+
};
|
337 |
+
});
|
338 |
+
|
339 |
+
// Add labels for major countries if showLabels is enabled
|
340 |
+
if (this.options.showLabels) {
|
341 |
+
Object.values(this.countries).forEach(country => {
|
342 |
+
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
343 |
+
label.setAttribute('x', country.x);
|
344 |
+
label.setAttribute('y', country.y - 5);
|
345 |
+
label.setAttribute('text-anchor', 'middle');
|
346 |
+
label.setAttribute('font-size', '9px');
|
347 |
+
label.setAttribute('fill', '#555');
|
348 |
+
label.textContent = country.name;
|
349 |
+
this.markersGroup.appendChild(label);
|
350 |
+
});
|
351 |
+
}
|
352 |
+
}
|
353 |
+
|
354 |
+
// Helper function to extract center point from SVG path
|
355 |
+
extractCenterFromPath(pathData) {
|
356 |
+
// This is a simplified approach - in a real implementation,
|
357 |
+
// you would parse the path data more carefully
|
358 |
+
const points = pathData.replace(/[A-Za-z]/g, ' ').trim().split(/\s+/).map(Number);
|
359 |
+
|
360 |
+
// Calculate average of x and y coordinates
|
361 |
+
let sumX = 0, sumY = 0, count = 0;
|
362 |
+
for (let i = 0; i < points.length; i += 2) {
|
363 |
+
if (i + 1 < points.length) {
|
364 |
+
sumX += points[i];
|
365 |
+
sumY += points[i + 1];
|
366 |
+
count++;
|
367 |
+
}
|
368 |
+
}
|
369 |
+
|
370 |
+
return { x: sumX / count, y: sumY / count };
|
371 |
+
}
|
372 |
+
|
373 |
+
// Converts longitude to X coordinate
|
374 |
+
longitudeToX(lng) {
|
375 |
+
// Map longitude (-180 to 180) to screen coordinates
|
376 |
+
const width = this.container.clientWidth;
|
377 |
+
return (lng + 180) * (width / 360);
|
378 |
+
}
|
379 |
+
|
380 |
+
// Converts latitude to Y coordinate
|
381 |
+
latitudeToY(lat) {
|
382 |
+
// Map latitude (-90 to 90) to screen coordinates
|
383 |
+
const height = this.container.clientHeight;
|
384 |
+
// Adjust for Mercator-like projection
|
385 |
+
const latRad = lat * Math.PI / 180;
|
386 |
+
const mercN = Math.log(Math.tan((Math.PI / 4) + (latRad / 2)));
|
387 |
+
return height / 2 - (height * mercN / (2 * Math.PI));
|
388 |
+
}
|
389 |
+
|
390 |
+
// Add a trade flow between two countries
|
391 |
+
addFlow(fromCountryCode, toCountryCode, value = 1, options = {}) {
|
392 |
+
if (!this.initialized) {
|
393 |
+
console.error('Map not initialized. Call initialize() first.');
|
394 |
+
return this;
|
395 |
+
}
|
396 |
+
|
397 |
+
const fromCountry = this.countries[fromCountryCode];
|
398 |
+
const toCountry = this.countries[toCountryCode];
|
399 |
+
|
400 |
+
if (!fromCountry || !toCountry) {
|
401 |
+
console.warn(`Country not found: ${!fromCountry ? fromCountryCode : toCountryCode}`);
|
402 |
+
return this;
|
403 |
+
}
|
404 |
+
|
405 |
+
// Scale the line width based on the trade value
|
406 |
+
let scaledWidth = this.options.flowLineWidth;
|
407 |
+
if (value > 0) {
|
408 |
+
// Log scale for better visualization
|
409 |
+
scaledWidth = this.options.flowLineWidth * (1 + 0.5 * Math.log10(1 + value / 1000));
|
410 |
+
}
|
411 |
+
|
412 |
+
// Highlight the selected countries
|
413 |
+
this.selectedCountries.add(fromCountryCode);
|
414 |
+
this.selectedCountries.add(toCountryCode);
|
415 |
+
|
416 |
+
if (fromCountry.element) {
|
417 |
+
fromCountry.element.setAttribute('fill', this.options.selectedCountryColor);
|
418 |
+
}
|
419 |
+
|
420 |
+
if (toCountry.element) {
|
421 |
+
toCountry.element.setAttribute('fill', this.options.selectedCountryColor);
|
422 |
+
}
|
423 |
+
|
424 |
+
// Create a unique flow ID
|
425 |
+
const flowId = `flow-${fromCountryCode}-${toCountryCode}`;
|
426 |
+
|
427 |
+
// Create a group for this flow
|
428 |
+
const flowGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
429 |
+
flowGroup.setAttribute('class', 'flow-connection');
|
430 |
+
flowGroup.setAttribute('data-flow-id', flowId);
|
431 |
+
flowGroup.setAttribute('data-from', fromCountryCode);
|
432 |
+
flowGroup.setAttribute('data-to', toCountryCode);
|
433 |
+
|
434 |
+
// Calculate the curved path between countries
|
435 |
+
// Use quadratic Bezier curve for smoother appearance
|
436 |
+
const dx = toCountry.x - fromCountry.x;
|
437 |
+
const dy = toCountry.y - fromCountry.y;
|
438 |
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
439 |
+
|
440 |
+
// Calculate curvature - higher for longer distances
|
441 |
+
const curveAmount = distance * 0.2;
|
442 |
+
|
443 |
+
// Calculate control point perpendicular to the line
|
444 |
+
const midX = (fromCountry.x + toCountry.x) / 2;
|
445 |
+
const midY = (fromCountry.y + toCountry.y) / 2;
|
446 |
+
|
447 |
+
// Calculate perpendicular unit vector
|
448 |
+
const perpX = -dy / distance;
|
449 |
+
const perpY = dx / distance;
|
450 |
+
|
451 |
+
// Position control point perpendicular to the midpoint
|
452 |
+
const ctrlX = midX + perpX * curveAmount;
|
453 |
+
const ctrlY = midY + perpY * curveAmount;
|
454 |
+
|
455 |
+
// Create the flow path
|
456 |
+
const flowPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
457 |
+
flowPath.setAttribute('class', 'flow-line');
|
458 |
+
flowPath.setAttribute('d', `M ${fromCountry.x} ${fromCountry.y} Q ${ctrlX} ${ctrlY} ${toCountry.x} ${toCountry.y}`);
|
459 |
+
flowPath.setAttribute('fill', 'none');
|
460 |
+
flowPath.setAttribute('stroke', `url(#flow-gradient-${this.containerId})`);
|
461 |
+
flowPath.setAttribute('stroke-width', options.width || scaledWidth);
|
462 |
+
flowPath.setAttribute('stroke-linecap', 'round');
|
463 |
+
flowPath.setAttribute('marker-end', `url(#arrow-${this.containerId})`);
|
464 |
+
|
465 |
+
// Add to flow group
|
466 |
+
flowGroup.appendChild(flowPath);
|
467 |
+
|
468 |
+
// Add flow value label if requested
|
469 |
+
if (options.showLabel !== false && value > 0) {
|
470 |
+
const valueFormatted = value >= 1000000 ?
|
471 |
+
`$${(value/1000000).toFixed(1)}M` :
|
472 |
+
`$${(value/1000).toFixed(0)}K`;
|
473 |
+
|
474 |
+
// Create label background
|
475 |
+
const labelBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
476 |
+
labelBg.setAttribute('x', ctrlX - 20);
|
477 |
+
labelBg.setAttribute('y', ctrlY - 10);
|
478 |
+
labelBg.setAttribute('width', 40);
|
479 |
+
labelBg.setAttribute('height', 20);
|
480 |
+
labelBg.setAttribute('rx', 3);
|
481 |
+
labelBg.setAttribute('ry', 3);
|
482 |
+
labelBg.setAttribute('fill', 'white');
|
483 |
+
labelBg.setAttribute('opacity', '0.8');
|
484 |
+
|
485 |
+
// Create label text
|
486 |
+
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
487 |
+
label.setAttribute('x', ctrlX);
|
488 |
+
label.setAttribute('y', ctrlY + 4);
|
489 |
+
label.setAttribute('text-anchor', 'middle');
|
490 |
+
label.setAttribute('font-size', '8px');
|
491 |
+
label.setAttribute('fill', '#444');
|
492 |
+
label.textContent = options.label || valueFormatted;
|
493 |
+
|
494 |
+
// Add to flow group
|
495 |
+
flowGroup.appendChild(labelBg);
|
496 |
+
flowGroup.appendChild(label);
|
497 |
+
}
|
498 |
+
|
499 |
+
// Add flow group to the flows layer
|
500 |
+
this.flowsGroup.appendChild(flowGroup);
|
501 |
+
|
502 |
+
// Add animated marker
|
503 |
+
if (options.animated !== false) {
|
504 |
+
this.animateFlowPath(flowPath, fromCountry, toCountry);
|
505 |
+
}
|
506 |
+
|
507 |
+
// Store flow data
|
508 |
+
this.flows.push({
|
509 |
+
id: flowId,
|
510 |
+
from: fromCountryCode,
|
511 |
+
to: toCountryCode,
|
512 |
+
value: value,
|
513 |
+
element: flowGroup,
|
514 |
+
path: flowPath
|
515 |
+
});
|
516 |
+
|
517 |
+
return this;
|
518 |
+
}
|
519 |
+
|
520 |
+
// Animate flow along a path with subtle elegant animation
|
521 |
+
animateFlowPath(path, source, target) {
|
522 |
+
// Create a group for the animated markers
|
523 |
+
const markerGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
524 |
+
markerGroup.setAttribute('class', 'flow-marker-group');
|
525 |
+
this.flowsGroup.appendChild(markerGroup);
|
526 |
+
|
527 |
+
// Create pulse effect marker
|
528 |
+
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
529 |
+
marker.setAttribute('class', 'flow-marker');
|
530 |
+
marker.setAttribute('r', 3);
|
531 |
+
marker.setAttribute('fill', `url(#pulse-gradient-${this.containerId})`);
|
532 |
+
marker.setAttribute('opacity', '0.8');
|
533 |
+
markerGroup.appendChild(marker);
|
534 |
+
|
535 |
+
// Create subtle glow effect
|
536 |
+
const glowMarker = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
537 |
+
glowMarker.setAttribute('class', 'flow-marker-glow');
|
538 |
+
glowMarker.setAttribute('r', 5);
|
539 |
+
glowMarker.setAttribute('fill', `url(#pulse-gradient-${this.containerId})`);
|
540 |
+
glowMarker.setAttribute('opacity', '0.3');
|
541 |
+
markerGroup.appendChild(glowMarker);
|
542 |
+
|
543 |
+
// Create animated motion
|
544 |
+
const animate = () => {
|
545 |
+
// Calculate the total length of the path for smoother animation
|
546 |
+
const pathLength = path.getTotalLength();
|
547 |
+
const duration = pathLength / 30 * (1 / this.options.animationSpeed);
|
548 |
+
|
549 |
+
// Animation function with smoother easing
|
550 |
+
const startTime = performance.now();
|
551 |
+
|
552 |
+
const step = (timestamp) => {
|
553 |
+
const elapsedTime = timestamp - startTime;
|
554 |
+
let progress = Math.min(elapsedTime / (duration * 1000), 1);
|
555 |
+
|
556 |
+
// Add slight easing for more elegant motion
|
557 |
+
progress = easeInOutQuad(progress);
|
558 |
+
|
559 |
+
// Get point at percentage along the path
|
560 |
+
const point = path.getPointAtLength(progress * pathLength);
|
561 |
+
|
562 |
+
// Update marker position
|
563 |
+
marker.setAttribute('cx', point.x);
|
564 |
+
marker.setAttribute('cy', point.y);
|
565 |
+
glowMarker.setAttribute('cx', point.x);
|
566 |
+
glowMarker.setAttribute('cy', point.y);
|
567 |
+
|
568 |
+
if (progress < 1) {
|
569 |
+
requestAnimationFrame(step);
|
570 |
+
} else {
|
571 |
+
// When animation completes, pause then restart
|
572 |
+
markerGroup.setAttribute('opacity', '0');
|
573 |
+
setTimeout(() => {
|
574 |
+
markerGroup.setAttribute('opacity', '1');
|
575 |
+
animate();
|
576 |
+
}, 1200);
|
577 |
+
}
|
578 |
+
};
|
579 |
+
|
580 |
+
requestAnimationFrame(step);
|
581 |
+
};
|
582 |
+
|
583 |
+
// Easing function for smoother animation
|
584 |
+
const easeInOutQuad = (t) => {
|
585 |
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
586 |
+
};
|
587 |
+
|
588 |
+
// Start the animation
|
589 |
+
animate();
|
590 |
+
|
591 |
+
return markerGroup;
|
592 |
+
}
|
593 |
+
|
594 |
+
// Clear all trade flows
|
595 |
+
clearFlows() {
|
596 |
+
// Remove flow elements
|
597 |
+
while (this.flowsGroup.firstChild) {
|
598 |
+
this.flowsGroup.removeChild(this.flowsGroup.firstChild);
|
599 |
+
}
|
600 |
+
|
601 |
+
// Reset country colors
|
602 |
+
this.selectedCountries.clear();
|
603 |
+
Object.values(this.countries).forEach(country => {
|
604 |
+
if (country.element) {
|
605 |
+
country.element.setAttribute('fill', this.options.landColor);
|
606 |
+
}
|
607 |
+
});
|
608 |
+
|
609 |
+
this.flows = [];
|
610 |
+
return this;
|
611 |
+
}
|
612 |
+
|
613 |
+
// Add demo trade flows to the map
|
614 |
+
addDemoFlows() {
|
615 |
+
// Clear existing flows
|
616 |
+
this.clearFlows();
|
617 |
+
|
618 |
+
// Add major trade flows with values (in millions USD)
|
619 |
+
this.addFlow('840', '156', 500000, { label: '$500B' }); // US to China
|
620 |
+
this.addFlow('840', '276', 180000, { label: '$180B' }); // US to Germany
|
621 |
+
this.addFlow('156', '392', 320000, { label: '$320B' }); // China to Japan
|
622 |
+
this.addFlow('156', '124', 110000, { label: '$110B' }); // China to Canada
|
623 |
+
this.addFlow('276', '250', 230000, { label: '$230B' }); // Germany to France
|
624 |
+
|
625 |
+
// Render all flows
|
626 |
+
return this;
|
627 |
+
}
|
628 |
+
|
629 |
+
// Update the map with new data
|
630 |
+
updateData(flows) {
|
631 |
+
this.clearFlows();
|
632 |
+
|
633 |
+
flows.forEach(flow => {
|
634 |
+
this.addFlow(flow.from || flow.reporterCode,
|
635 |
+
flow.to || flow.partnerCode,
|
636 |
+
flow.value || flow.tradeValue,
|
637 |
+
flow.options || {});
|
638 |
+
});
|
639 |
+
|
640 |
+
return this;
|
641 |
+
}
|
642 |
+
|
643 |
+
// Resize the map
|
644 |
+
resize(width, height) {
|
645 |
+
if (width) this.container.style.width = width;
|
646 |
+
if (height) this.container.style.height = `${height}px`;
|
647 |
+
return this;
|
648 |
+
}
|
649 |
+
}
|
650 |
+
|
651 |
+
// Initialize maps when document is ready
|
652 |
+
document.addEventListener('DOMContentLoaded', () => {
|
653 |
+
// Create a global object to store map instances
|
654 |
+
window.tradeFlowMaps = {};
|
655 |
+
|
656 |
+
// Initialize trade flow maps on the page
|
657 |
+
const mapContainers = document.querySelectorAll('.trade-flow-map');
|
658 |
+
|
659 |
+
mapContainers.forEach(container => {
|
660 |
+
const containerId = container.id;
|
661 |
+
if (!containerId) return;
|
662 |
+
|
663 |
+
const options = {
|
664 |
+
showLabels: container.dataset.showLabels === 'true',
|
665 |
+
showFlowValues: container.dataset.showValues !== 'false',
|
666 |
+
height: parseInt(container.dataset.height || 350, 10)
|
667 |
+
};
|
668 |
+
|
669 |
+
// Create a new map instance
|
670 |
+
const map = new TradeFlowMap(containerId, options);
|
671 |
+
map.initialize().then(() => {
|
672 |
+
// Add demo flows if requested
|
673 |
+
if (container.dataset.demo === 'true') {
|
674 |
+
// Use our modern demo flows with proper styling
|
675 |
+
map.addDemoFlows();
|
676 |
+
}
|
677 |
+
});
|
678 |
+
|
679 |
+
// Store the map instance
|
680 |
+
window.tradeFlowMaps[containerId] = map;
|
681 |
+
});
|
682 |
+
});
|