jomasego commited on
Commit
ba5fa26
·
verified ·
1 Parent(s): c3c7204

Upload static/js/tradeflowmap.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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
+ });