jomasego commited on
Commit
d657099
·
verified ·
1 Parent(s): 44cb0eb

Upload static/js/main.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. static/js/main.js +1216 -0
static/js/main.js ADDED
@@ -0,0 +1,1216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ // --- Exports by Country Tab Logic ---
3
+ // --- Imports by Country Tab Logic ---
4
+ // --- Exports by Product Tab Logic ---
5
+ // --- Imports by Product Tab Logic ---
6
+ // --- Rankings Tab Logic ---
7
+ // --- Bilateral Trade Tab Logic ---
8
+ // --- Data Download Tab Logic ---
9
+ const dataDownloadForm = document.getElementById('dataDownloadForm');
10
+ const dataDownloadStatus = document.getElementById('dataDownloadStatus');
11
+ const dataDownloadChartDiv = document.createElement('div');
12
+ dataDownloadChartDiv.id = 'dataDownloadChart';
13
+ dataDownloadChartDiv.style.marginTop = '2em';
14
+ dataDownloadStatus && dataDownloadStatus.parentNode.insertBefore(dataDownloadChartDiv, dataDownloadStatus.nextSibling);
15
+ let dataDownloadChartData = null;
16
+ if (dataDownloadForm) {
17
+ dataDownloadForm.addEventListener('submit', async function(e) {
18
+ e.preventDefault();
19
+ dataDownloadStatus.innerHTML = '';
20
+ dataDownloadChartDiv.innerHTML = '';
21
+ // ...existing fetch logic...
22
+ // After successful data fetch:
23
+ // dataDownloadChartData = fetchedData;
24
+ // renderDataDownloadChart(fetchedData);
25
+ });
26
+ }
27
+ // Render chart for Data Download tab (all rows)
28
+ // Generalized chart rendering for any tab
29
+ function renderModernChart(rows, chartDivId) {
30
+ const chartDiv = document.getElementById(chartDivId);
31
+ if (!chartDiv || !Array.isArray(rows) || rows.length === 0) return;
32
+ chartDiv.innerHTML = '';
33
+ const canvas = document.createElement('canvas');
34
+ canvas.width = Math.min(900, window.innerWidth * 0.96);
35
+ canvas.height = 340;
36
+ canvas.style.background = '#fff';
37
+ canvas.style.borderRadius = '10px';
38
+ canvas.style.boxShadow = '0 2px 10px rgba(25,118,210,0.07)';
39
+ chartDiv.appendChild(canvas);
40
+ const ctx = canvas.getContext('2d');
41
+ // Try to find year and value columns
42
+ let years = [], values = [];
43
+ if (rows[0].year !== undefined && rows[0].value !== undefined) {
44
+ years = rows.map(r => +r.year);
45
+ values = rows.map(r => +r.value);
46
+ } else if (rows[0].hasOwnProperty('country') && rows[0].hasOwnProperty('value')) {
47
+ // For country rankings
48
+ years = rows.map((_, i) => i+1); // Rank as x-axis
49
+ values = rows.map(r => +r.value);
50
+ } else if (rows[0].hasOwnProperty('cmdCode') && rows[0].hasOwnProperty('value')) {
51
+ // For product by code
52
+ years = rows.map((_, i) => i+1);
53
+ values = rows.map(r => +r.value);
54
+ } else {
55
+ return;
56
+ }
57
+ const minYear = Math.min(...years), maxYear = Math.max(...years);
58
+ const minVal = Math.min(...values), maxVal = Math.max(...values);
59
+ // Draw grid
60
+ ctx.strokeStyle = '#e3e9f6';
61
+ ctx.lineWidth = 1;
62
+ for (let i = 0; i <= 5; ++i) {
63
+ let y = 40 + i * (260 / 5);
64
+ ctx.beginPath();
65
+ ctx.moveTo(60, y);
66
+ ctx.lineTo(canvas.width - 30, y);
67
+ ctx.stroke();
68
+ }
69
+ // Axes
70
+ ctx.strokeStyle = '#1976d2';
71
+ ctx.lineWidth = 2;
72
+ ctx.beginPath();
73
+ ctx.moveTo(60, 40);
74
+ ctx.lineTo(60, 300);
75
+ ctx.lineTo(canvas.width - 30, 300);
76
+ ctx.stroke();
77
+ // Y labels
78
+ ctx.fillStyle = '#34495e';
79
+ ctx.font = '13px Segoe UI, Arial, sans-serif';
80
+ for (let i = 0; i <= 5; ++i) {
81
+ let v = minVal + (maxVal - minVal) * i / 5;
82
+ let y = 300 - (v - minVal) / (maxVal - minVal) * 260;
83
+ ctx.fillText(v.toFixed(0), 10, y + 4);
84
+ }
85
+ // X labels
86
+ for (let i = 0; i < years.length; ++i) {
87
+ let x = 60 + (years[i] - minYear) / (maxYear - minYear || 1) * (canvas.width - 90);
88
+ let label = (rows[0].country && rows[i].country) ? rows[i].country : (rows[0].cmdCode && rows[i].cmdCode ? rows[i].cmdCode : years[i]);
89
+ ctx.fillText(label, x - 12, 320);
90
+ }
91
+ // Draw line
92
+ ctx.strokeStyle = '#42a5f5';
93
+ ctx.lineWidth = 3;
94
+ ctx.beginPath();
95
+ for (let i = 0; i < years.length; ++i) {
96
+ let x = 60 + (years[i] - minYear) / (maxYear - minYear || 1) * (canvas.width - 90);
97
+ let y = 300 - (values[i] - minVal) / (maxVal - minVal || 1) * 260;
98
+ if (i === 0) ctx.moveTo(x, y);
99
+ else ctx.lineTo(x, y);
100
+ }
101
+ ctx.stroke();
102
+ // Draw points
103
+ for (let i = 0; i < years.length; ++i) {
104
+ let x = 60 + (years[i] - minYear) / (maxYear - minYear || 1) * (canvas.width - 90);
105
+ let y = 300 - (values[i] - minVal) / (maxVal - minVal || 1) * 260;
106
+ ctx.beginPath();
107
+ ctx.arc(x, y, 5, 0, 2 * Math.PI);
108
+ ctx.fillStyle = '#1976d2';
109
+ ctx.fill();
110
+ }
111
+ // Tooltip (simple hover)
112
+ canvas.onmousemove = function(ev) {
113
+ const rect = canvas.getBoundingClientRect();
114
+ const mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
115
+ let found = -1;
116
+ for (let i = 0; i < years.length; ++i) {
117
+ let x = 60 + (years[i] - minYear) / (maxYear - minYear || 1) * (canvas.width - 90);
118
+ let y = 300 - (values[i] - minVal) / (maxVal - minVal || 1) * 260;
119
+ if (Math.abs(mx - x) < 8 && Math.abs(my - y) < 8) { found = i; break; }
120
+ }
121
+ chartDiv.querySelectorAll('.chart-tooltip').forEach(e => e.remove());
122
+ if (found !== -1) {
123
+ const tip = document.createElement('div');
124
+ tip.className = 'chart-tooltip';
125
+ tip.style.position = 'absolute';
126
+ tip.style.left = (mx + 10) + 'px';
127
+ tip.style.top = (my + 10) + 'px';
128
+ tip.style.background = '#fff';
129
+ tip.style.border = '1px solid #1976d2';
130
+ tip.style.borderRadius = '6px';
131
+ tip.style.padding = '6px 12px';
132
+ tip.style.boxShadow = '0 2px 8px rgba(25,118,210,0.15)';
133
+ tip.style.pointerEvents = 'none';
134
+ tip.style.fontSize = '13px';
135
+ tip.style.zIndex = 1000;
136
+ tip.innerHTML = `<b>${(rows[found].country || rows[found].cmdCode || 'Year')}:</b> ${years[found]}<br><b>Value:</b> ${values[found]}`;
137
+ chartDiv.appendChild(tip);
138
+ }
139
+ };
140
+ canvas.onmouseleave = function() {
141
+ chartDiv.querySelectorAll('.chart-tooltip').forEach(e => e.remove());
142
+ };
143
+ }
144
+ // Backward compat: keep for Data Download tab
145
+ function renderDataDownloadChart(rows) {
146
+ renderModernChart(rows, 'dataDownloadChart');
147
+ }
148
+
149
+ const predictionForm = document.getElementById('predictionForm');
150
+ const predictionReporter = document.getElementById('predictionReporter');
151
+ const predictionPartner = document.getElementById('predictionPartner');
152
+ const predictionResults = document.getElementById('predictionResults');
153
+ const predictionChart = document.getElementById('predictionChart');
154
+ const predictionDownloadBtn = document.getElementById('predictionDownloadBtn');
155
+ let predictionTableData = null;
156
+ // Populate country dropdowns
157
+ if (predictionReporter && predictionPartner && typeof COUNTRY_CODES !== 'undefined') {
158
+ predictionReporter.innerHTML = '';
159
+ predictionPartner.innerHTML = '';
160
+ COUNTRY_CODES.forEach(c => {
161
+ const opt1 = document.createElement('option');
162
+ opt1.value = c.code;
163
+ opt1.textContent = c.name + ' (' + c.code + ')';
164
+ predictionReporter.appendChild(opt1);
165
+ const opt2 = document.createElement('option');
166
+ opt2.value = c.code;
167
+ opt2.textContent = c.name + ' (' + c.code + ')';
168
+ predictionPartner.appendChild(opt2);
169
+ });
170
+ }
171
+ if (predictionForm) {
172
+ predictionForm.addEventListener('submit', async function(e) {
173
+ e.preventDefault();
174
+ predictionResults.innerHTML = '';
175
+ predictionDownloadBtn.style.display = 'none';
176
+ if (predictionChart) predictionChart.style.display = 'none';
177
+ const reporterCode = predictionReporter.value;
178
+ const partnerCode = predictionPartner.value;
179
+ const year = document.getElementById('predictionYear').value;
180
+ const cmdCode = document.getElementById('predictionCommodity').value;
181
+ const modelType = document.getElementById('predictionModel').value;
182
+ const payload = {
183
+ reporterCode: reporterCode,
184
+ partnerCode: partnerCode,
185
+ period: year,
186
+ cmdCode: cmdCode,
187
+ flowCode: '',
188
+ modelType: modelType
189
+ };
190
+ predictionResults.innerHTML = '<div>Predicting...</div>';
191
+ showSpinner();
192
+ try {
193
+ const resp = await fetch('/api/predict', {
194
+ method: 'POST',
195
+ headers: { 'Content-Type': 'application/json' },
196
+ body: JSON.stringify(payload)
197
+ });
198
+ const data = await resp.json();
199
+ if (data && data.historical && data.prediction) {
200
+ // Prepare table data
201
+ let rows = [];
202
+ data.historical.forEach(row => {
203
+ rows.push({ year: row.year, value: row.value, type: 'historical' });
204
+ });
205
+ rows.push({ year: data.prediction.year, value: data.prediction.value, type: 'predicted' });
206
+ predictionTableData = rows;
207
+ // Render table
208
+ let html = '<div style="overflow-x:auto;"><table><thead><tr><th>Year</th><th>Value</th><th>Type</th></tr></thead><tbody>';
209
+ rows.forEach(row => {
210
+ html += `<tr><td>${row.year}</td><td>${row.value}</td><td>${row.type}</td></tr>`;
211
+ });
212
+ html += '</tbody></table></div>';
213
+ predictionResults.innerHTML = html;
214
+ predictionDownloadBtn.style.display = 'inline-block';
215
+ // Plot chart (vanilla JS, use Canvas API)
216
+ plotPredictionChart(rows);
217
+ } else {
218
+ predictionResults.innerHTML = '<div>No prediction data returned.</div>';
219
+ }
220
+ } catch (err) {
221
+ predictionResults.innerHTML = '<div>Error fetching prediction.</div>';
222
+ } finally {
223
+ hideSpinner();
224
+ }
225
+ });
226
+ predictionDownloadBtn.addEventListener('click', function() {
227
+ if (!predictionTableData) return;
228
+ let csv = 'Year,Value,Type\n';
229
+ predictionTableData.forEach(row => {
230
+ csv += `${row.year},${row.value},${row.type}\n`;
231
+ });
232
+ const blob = new Blob([csv], {type: 'text/csv'});
233
+ const url = URL.createObjectURL(blob);
234
+ const a = document.createElement('a');
235
+ a.href = url;
236
+ a.download = 'trade_prediction.csv';
237
+ document.body.appendChild(a);
238
+ a.click();
239
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
240
+ });
241
+ }
242
+ // Simple chart plotting function (vanilla JS, Canvas API)
243
+ function plotPredictionChart(rows) {
244
+ if (!predictionChart) return;
245
+ const width = 600, height = 320, pad = 50;
246
+ predictionChart.width = width;
247
+ predictionChart.height = height;
248
+ const ctx = predictionChart.getContext('2d');
249
+ ctx.clearRect(0, 0, width, height);
250
+ // Prepare data
251
+ const years = rows.map(r => +r.year);
252
+ const values = rows.map(r => +r.value);
253
+ const minYear = Math.min(...years), maxYear = Math.max(...years);
254
+ const minVal = Math.min(...values), maxVal = Math.max(...values);
255
+ // Axes
256
+ ctx.strokeStyle = '#888';
257
+ ctx.lineWidth = 1;
258
+ ctx.beginPath();
259
+ ctx.moveTo(pad, pad);
260
+ ctx.lineTo(pad, height-pad);
261
+ ctx.lineTo(width-pad, height-pad);
262
+ ctx.stroke();
263
+ // Y labels
264
+ ctx.fillStyle = '#444';
265
+ ctx.font = '13px sans-serif';
266
+ for (let i=0; i<=4; ++i) {
267
+ let v = minVal + (maxVal-minVal)*i/4;
268
+ let y = height-pad - (v-minVal)/(maxVal-minVal)*(height-2*pad);
269
+ ctx.fillText(v.toFixed(0), 6, y+4);
270
+ }
271
+ // X labels
272
+ for (let i=0; i<years.length; ++i) {
273
+ let x = pad + (years[i]-minYear)/(maxYear-minYear)*(width-2*pad);
274
+ ctx.fillText(years[i], x-10, height-pad+18);
275
+ }
276
+ // Plot historical
277
+ ctx.strokeStyle = '#1976d2';
278
+ ctx.lineWidth = 2;
279
+ ctx.beginPath();
280
+ for (let i=0; i<rows.length; ++i) {
281
+ if (rows[i].type !== 'historical') continue;
282
+ let x = pad + (rows[i].year-minYear)/(maxYear-minYear)*(width-2*pad);
283
+ let y = height-pad - (rows[i].value-minVal)/(maxVal-minVal)*(height-2*pad);
284
+ if (i===0) ctx.moveTo(x, y);
285
+ else ctx.lineTo(x, y);
286
+ }
287
+ ctx.stroke();
288
+ // Plot predicted
289
+ let pred = rows.find(r => r.type==='predicted');
290
+ if (pred) {
291
+ let x = pad + (pred.year-minYear)/(maxYear-minYear)*(width-2*pad);
292
+ let y = height-pad - (pred.value-minVal)/(maxVal-minVal)*(height-2*pad);
293
+ ctx.fillStyle = '#e53935';
294
+ ctx.beginPath();
295
+ ctx.arc(x, y, 7, 0, 2*Math.PI);
296
+ ctx.fill();
297
+ ctx.font = 'bold 14px sans-serif';
298
+ ctx.fillText('Prediction', x+10, y-10);
299
+ }
300
+ predictionChart.style.display = 'block';
301
+ }
302
+
303
+ // Already declared at the top:
304
+ // const dataDownloadForm = document.getElementById('dataDownloadForm');
305
+ const dataDownloadReporter = document.getElementById('dataDownloadReporter');
306
+ const dataDownloadPartner = document.getElementById('dataDownloadPartner');
307
+ // const dataDownloadStatus = document.getElementById('dataDownloadStatus');
308
+ if (dataDownloadReporter && typeof COUNTRY_CODES !== 'undefined') {
309
+ dataDownloadReporter.innerHTML = '<option value="">All</option>';
310
+ COUNTRY_CODES.forEach(c => {
311
+ const opt = document.createElement('option');
312
+ opt.value = c.code;
313
+ opt.textContent = c.name + ' (' + c.code + ')';
314
+ dataDownloadReporter.appendChild(opt);
315
+ });
316
+ }
317
+ if (dataDownloadPartner && typeof COUNTRY_CODES !== 'undefined') {
318
+ dataDownloadPartner.innerHTML = '<option value="">All</option>';
319
+ COUNTRY_CODES.forEach(c => {
320
+ const opt = document.createElement('option');
321
+ opt.value = c.code;
322
+ opt.textContent = c.name + ' (' + c.code + ')';
323
+ dataDownloadPartner.appendChild(opt);
324
+ });
325
+ }
326
+ if (dataDownloadForm) {
327
+ dataDownloadForm.addEventListener('submit', async function(e) {
328
+ e.preventDefault();
329
+ dataDownloadStatus.innerHTML = '';
330
+ const reporterCode = dataDownloadReporter.value;
331
+ const partnerCode = dataDownloadPartner.value;
332
+ const year = document.getElementById('dataDownloadYear').value;
333
+ const cmdCode = document.getElementById('dataDownloadCommodity').value;
334
+ const flowCode = document.getElementById('dataDownloadFlow').value;
335
+ // Determine all reporter/partner combos
336
+ let reporterList = reporterCode ? [reporterCode] : COUNTRY_CODES.map(c => c.code);
337
+ let partnerList = partnerCode ? [partnerCode] : COUNTRY_CODES.map(c => c.code);
338
+ // Prevent massive downloads (limit combos)
339
+ if (reporterList.length * partnerList.length > 200) {
340
+ dataDownloadStatus.innerHTML = '<div style="color:red;">Too many combinations selected! Please narrow your selection.</div>';
341
+ return;
342
+ }
343
+ dataDownloadStatus.innerHTML = '<div>Fetching data...</div>';
344
+ let allRows = [];
345
+ let columnsSet = new Set();
346
+ for (let r of reporterList) {
347
+ for (let p of partnerList) {
348
+ if (r === p) continue; // skip self-pairs
349
+ const payload = {
350
+ reporterCode: r,
351
+ partnerCode: p,
352
+ period: year,
353
+ cmdCode: cmdCode,
354
+ flowCode: flowCode
355
+ };
356
+ try {
357
+ const resp = await fetch('/api/trade', {
358
+ method: 'POST',
359
+ headers: { 'Content-Type': 'application/json' },
360
+ body: JSON.stringify(payload)
361
+ });
362
+ const data = await resp.json();
363
+ if (data && data.rows && data.rows.length > 0) {
364
+ data.rows.forEach(row => {
365
+ allRows.push(row);
366
+ });
367
+ data.columns.forEach(col => columnsSet.add(col));
368
+ }
369
+ } catch (err) {
370
+ // skip errors
371
+ }
372
+ }
373
+ }
374
+ if (allRows.length === 0) {
375
+ dataDownloadStatus.innerHTML = '<div>No data found for your selection.</div>';
376
+ return;
377
+ }
378
+ // Build CSV
379
+ const columns = Array.from(columnsSet);
380
+ let csv = columns.join(',') + '\n';
381
+ allRows.forEach(row => {
382
+ csv += columns.map(col => row[col] !== undefined ? row[col] : '').join(',') + '\n';
383
+ });
384
+ const blob = new Blob([csv], {type: 'text/csv'});
385
+ const url = URL.createObjectURL(blob);
386
+ const a = document.createElement('a');
387
+ a.href = url;
388
+ a.download = 'custom_trade_data.csv';
389
+ document.body.appendChild(a);
390
+ a.click();
391
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
392
+ dataDownloadStatus.innerHTML = '<div>Download started.</div>';
393
+ });
394
+ }
395
+
396
+ const bilateralForm = document.getElementById('bilateralForm');
397
+ const bilateralReporter = document.getElementById('bilateralReporter');
398
+ const bilateralPartner = document.getElementById('bilateralPartner');
399
+ const bilateralResults = document.getElementById('bilateralResults');
400
+ const bilateralDownloadBtn = document.getElementById('bilateralDownloadBtn');
401
+ let bilateralTableData = null;
402
+ // Populate both country dropdowns
403
+ if (bilateralReporter && bilateralPartner && typeof COUNTRY_CODES !== 'undefined') {
404
+ bilateralReporter.innerHTML = '';
405
+ bilateralPartner.innerHTML = '';
406
+ COUNTRY_CODES.forEach(c => {
407
+ const opt1 = document.createElement('option');
408
+ opt1.value = c.code;
409
+ opt1.textContent = c.name + ' (' + c.code + ')';
410
+ bilateralReporter.appendChild(opt1);
411
+ const opt2 = document.createElement('option');
412
+ opt2.value = c.code;
413
+ opt2.textContent = c.name + ' (' + c.code + ')';
414
+ bilateralPartner.appendChild(opt2);
415
+ });
416
+ }
417
+ if (bilateralForm) {
418
+ bilateralForm.addEventListener('submit', async function(e) {
419
+ e.preventDefault();
420
+ bilateralResults.innerHTML = '';
421
+ bilateralDownloadBtn.style.display = 'none';
422
+ const reporterCode = bilateralReporter.value;
423
+ const partnerCode = bilateralPartner.value;
424
+ const year = document.getElementById('bilateralYear').value;
425
+ const cmdCode = document.getElementById('bilateralCommodity').value;
426
+ const payload = {
427
+ reporterCode: reporterCode,
428
+ partnerCode: partnerCode,
429
+ period: year,
430
+ cmdCode: cmdCode,
431
+ flowCode: '' // Show all flows
432
+ };
433
+ bilateralResults.innerHTML = '<div>Loading bilateral trade data...</div>';
434
+ try {
435
+ const resp = await fetch('/api/trade', {
436
+ method: 'POST',
437
+ headers: { 'Content-Type': 'application/json' },
438
+ body: JSON.stringify(payload)
439
+ });
440
+ const data = await resp.json();
441
+ if (data && data.rows && data.rows.length > 0) {
442
+ // Find value and flow columns
443
+ const valueCol = data.columns.includes('primaryValue') ? 'primaryValue' : (data.columns.includes('TradeValue') ? 'TradeValue' : (data.columns.includes('Value') ? 'Value' : null));
444
+ const flowCol = data.columns.includes('flowCode') ? 'flowCode' : (data.columns.includes('TradeFlow') ? 'TradeFlow' : null);
445
+ if (!valueCol) {
446
+ bilateralResults.innerHTML = '<div>No value column found.</div>';
447
+ return;
448
+ }
449
+ bilateralTableData = data.rows;
450
+ // Render table
451
+ let html = '<div style="overflow-x:auto;"><table><thead><tr>';
452
+ if (flowCol) html += '<th>Flow</th>';
453
+ html += '<th>Value</th></tr></thead><tbody>';
454
+ data.rows.forEach(row => {
455
+ html += '<tr>';
456
+ if (flowCol) html += `<td>${row[flowCol]}</td>`;
457
+ html += `<td>${row[valueCol]}</td></tr>`;
458
+ });
459
+ html += '</tbody></table></div>';
460
+ bilateralResults.innerHTML = html;
461
+ if (data.rows.length > 0) bilateralDownloadBtn.style.display = 'inline-block';
462
+ else bilateralDownloadBtn.style.display = 'none';
463
+ // Modern chart for Bilateral
464
+ renderModernChart(data.rows, 'bilateralChart');
465
+ } else {
466
+ bilateralResults.innerHTML = '<div>No data found for this country pair/year.</div>';
467
+ bilateralDownloadBtn.style.display = 'none';
468
+ }
469
+ } catch (err) {
470
+ bilateralResults.innerHTML = '<div>Error fetching data.</div>';
471
+ } finally {
472
+ hideSpinner();
473
+ }
474
+ });
475
+ bilateralDownloadBtn.addEventListener('click', function() {
476
+ if (!bilateralTableData) return;
477
+ let csv = 'Flow,Value\n';
478
+ bilateralTableData.forEach(row => {
479
+ csv += `${row.flowCode || row.TradeFlow || ''},${row.primaryValue || row.TradeValue || row.Value || ''}\n`;
480
+ });
481
+ const blob = new Blob([csv], {type: 'text/csv'});
482
+ const url = URL.createObjectURL(blob);
483
+ const a = document.createElement('a');
484
+ a.href = url;
485
+ a.download = 'bilateral_trade.csv';
486
+ document.body.appendChild(a);
487
+ a.click();
488
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
489
+ });
490
+ }
491
+
492
+ const rankingsForm = document.getElementById('rankingsForm');
493
+ const rankingsResults = document.getElementById('rankingsResults');
494
+ const rankingsDownloadBtn = document.getElementById('rankingsDownloadBtn');
495
+ let rankingsTableData = null;
496
+ if (rankingsForm) {
497
+ rankingsForm.addEventListener('submit', async function(e) {
498
+ e.preventDefault();
499
+ rankingsResults.innerHTML = '';
500
+ rankingsDownloadBtn.style.display = 'none';
501
+ const year = document.getElementById('rankingsYear').value;
502
+ const cmdCode = document.getElementById('rankingsCommodity').value;
503
+ const flowCode = document.getElementById('rankingsFlow').value;
504
+ // Fetch for all countries: iterate COUNTRY_CODES
505
+ const allPromises = COUNTRY_CODES.map(async country => {
506
+ const payload = {
507
+ reporterCode: country.code,
508
+ partnerCode: '0', // World
509
+ period: year,
510
+ cmdCode: cmdCode,
511
+ flowCode: flowCode
512
+ };
513
+ try {
514
+ const resp = await fetch('/api/trade', {
515
+ method: 'POST',
516
+ headers: { 'Content-Type': 'application/json' },
517
+ body: JSON.stringify(payload)
518
+ });
519
+ const data = await resp.json();
520
+ if (data && data.rows && data.rows.length > 0) {
521
+ // Find the value column (primaryValue or TradeValue or Value)
522
+ const valueCol = data.columns.includes('primaryValue') ? 'primaryValue' : (data.columns.includes('TradeValue') ? 'TradeValue' : (data.columns.includes('Value') ? 'Value' : null));
523
+ const val = valueCol ? data.rows[0][valueCol] : null;
524
+ return {
525
+ country: country.name,
526
+ code: country.code,
527
+ value: val
528
+ };
529
+ } else {
530
+ return {
531
+ country: country.name,
532
+ code: country.code,
533
+ value: null
534
+ };
535
+ }
536
+ } catch (err) {
537
+ return {
538
+ country: country.name,
539
+ code: country.code,
540
+ value: null
541
+ };
542
+ }
543
+ });
544
+ rankingsResults.innerHTML = '<div>Loading data for all countries...</div>';
545
+ showSpinner();
546
+ const allResults = await Promise.all(allPromises);
547
+ hideSpinner();
548
+ // Filter for non-null values and sort descending
549
+ const filtered = allResults.filter(r => r.value !== null && r.value !== undefined).sort((a, b) => b.value - a.value);
550
+ rankingsTableData = filtered;
551
+ // Render table
552
+ let html = '<div style="overflow-x:auto;"><table><thead><tr><th>Country</th><th>Code</th><th>Value</th></tr></thead><tbody>';
553
+ filtered.forEach(row => {
554
+ html += `<tr><td>${row.country}</td><td>${row.code}</td><td>${row.value}</td></tr>`;
555
+ });
556
+ html += '</tbody></table></div>';
557
+ rankingsResults.innerHTML = html;
558
+ if (filtered.length > 0) rankingsDownloadBtn.style.display = 'inline-block';
559
+ else rankingsDownloadBtn.style.display = 'none';
560
+ // Modern chart for Rankings
561
+ renderModernChart(filtered, 'rankingsChart');
562
+ });
563
+ rankingsDownloadBtn.addEventListener('click', function() {
564
+ if (!rankingsTableData) return;
565
+ let csv = 'Country,Code,Value\n';
566
+ rankingsTableData.forEach(row => {
567
+ csv += `${row.country},${row.code},${row.value}\n`;
568
+ });
569
+ const blob = new Blob([csv], {type: 'text/csv'});
570
+ const url = URL.createObjectURL(blob);
571
+ const a = document.createElement('a');
572
+ a.href = url;
573
+ a.download = 'rankings.csv';
574
+ document.body.appendChild(a);
575
+ a.click();
576
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
577
+ });
578
+ }
579
+
580
+ const importsProductForm = document.getElementById('importsProductForm');
581
+ const importsProductCountry = document.getElementById('importsProductCountry');
582
+ const importsProductResults = document.getElementById('importsProductResults');
583
+ const importsProductDownloadBtn = document.getElementById('importsProductDownloadBtn');
584
+ let importsProductTableData = null;
585
+ // Populate country dropdown
586
+ if (importsProductCountry && typeof COUNTRY_CODES !== 'undefined') {
587
+ importsProductCountry.innerHTML = '';
588
+ COUNTRY_CODES.forEach(c => {
589
+ const opt = document.createElement('option');
590
+ opt.value = c.code;
591
+ opt.textContent = c.name + ' (' + c.code + ')';
592
+ importsProductCountry.appendChild(opt);
593
+ });
594
+ }
595
+ if (importsProductForm) {
596
+ importsProductForm.addEventListener('submit', async function(e) {
597
+ e.preventDefault();
598
+ importsProductResults.innerHTML = '';
599
+ importsProductDownloadBtn.style.display = 'none';
600
+ const reporterCode = importsProductCountry.value;
601
+ const year = document.getElementById('importsProductYear').value;
602
+ // Fetch for all products (HS codes) for this country/year
603
+ const payload = {
604
+ reporterCode: reporterCode,
605
+ partnerCode: '0', // World
606
+ period: year,
607
+ cmdCode: 'ALL', // Get all products
608
+ flowCode: 'M'
609
+ };
610
+ importsProductResults.innerHTML = '<div>Loading data for all products...</div>';
611
+ showSpinner();
612
+ try {
613
+ const resp = await fetch('/api/trade', {
614
+ method: 'POST',
615
+ headers: { 'Content-Type': 'application/json' },
616
+ body: JSON.stringify(payload)
617
+ });
618
+ const data = await resp.json();
619
+ if (data && data.rows && data.rows.length > 0) {
620
+ // Find the product/HS code column
621
+ const hsCol = data.columns.includes('cmdCode') ? 'cmdCode' : (data.columns.includes('productCode') ? 'productCode' : null);
622
+ const descCol = data.columns.includes('cmdDescE') ? 'cmdDescE' : (data.columns.includes('productDesc') ? 'productDesc' : null);
623
+ const valueCol = data.columns.includes('primaryValue') ? 'primaryValue' : (data.columns.includes('TradeValue') ? 'TradeValue' : (data.columns.includes('Value') ? 'Value' : null));
624
+ if (!hsCol || !valueCol) {
625
+ importsProductResults.innerHTML = '<div>No product/value columns found.</div>';
626
+ return;
627
+ }
628
+ // Sort by value descending
629
+ const sorted = data.rows.slice().sort((a, b) => b[valueCol] - a[valueCol]);
630
+ importsProductTableData = sorted;
631
+ // Render table
632
+ let html = '<div style="overflow-x:auto;"><table><thead><tr><th>HS Code</th>';
633
+ if (descCol) html += '<th>Description</th>';
634
+ html += '<th>Value</th></tr></thead><tbody>';
635
+ sorted.forEach(row => {
636
+ html += `<tr><td>${row[hsCol]}</td>`;
637
+ if (descCol) html += `<td>${row[descCol]}</td>`;
638
+ html += `<td>${row[valueCol]}</td></tr>`;
639
+ });
640
+ html += '</tbody></table></div>';
641
+ importsProductResults.innerHTML = html;
642
+ // Modern chart for Imports by Product
643
+ renderModernChart(sorted, 'importsProductChart');
644
+ if (sorted.length > 0) importsProductDownloadBtn.style.display = 'inline-block';
645
+ else importsProductDownloadBtn.style.display = 'none';
646
+ } else {
647
+ importsProductResults.innerHTML = '<div>No data found for this country/year.</div>';
648
+ importsProductDownloadBtn.style.display = 'none';
649
+ }
650
+ } catch (err) {
651
+ importsProductResults.innerHTML = '<div>Error fetching data.</div>';
652
+ } finally {
653
+ hideSpinner();
654
+ }
655
+ });
656
+ importsProductDownloadBtn.addEventListener('click', function() {
657
+ if (!importsProductTableData) return;
658
+ let csv = 'HS Code,Description,Value\n';
659
+ importsProductTableData.forEach(row => {
660
+ csv += `${row.cmdCode || row.productCode || ''},${row.cmdDescE || row.productDesc || ''},${row.primaryValue || row.TradeValue || row.Value || ''}\n`;
661
+ });
662
+ const blob = new Blob([csv], {type: 'text/csv'});
663
+ const url = URL.createObjectURL(blob);
664
+ const a = document.createElement('a');
665
+ a.href = url;
666
+ a.download = 'imports_by_product.csv';
667
+ document.body.appendChild(a);
668
+ a.click();
669
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
670
+ });
671
+ }
672
+
673
+ const exportsProductForm = document.getElementById('exportsProductForm');
674
+ const exportsProductCountry = document.getElementById('exportsProductCountry');
675
+ const exportsProductResults = document.getElementById('exportsProductResults');
676
+ const exportsProductDownloadBtn = document.getElementById('exportsProductDownloadBtn');
677
+ let exportsProductTableData = null;
678
+ // Populate country dropdown
679
+ if (exportsProductCountry && typeof COUNTRY_CODES !== 'undefined') {
680
+ exportsProductCountry.innerHTML = '';
681
+ COUNTRY_CODES.forEach(c => {
682
+ const opt = document.createElement('option');
683
+ opt.value = c.code;
684
+ opt.textContent = c.name + ' (' + c.code + ')';
685
+ exportsProductCountry.appendChild(opt);
686
+ });
687
+ }
688
+ if (exportsProductForm) {
689
+ exportsProductForm.addEventListener('submit', async function(e) {
690
+ e.preventDefault();
691
+ exportsProductResults.innerHTML = '';
692
+ exportsProductDownloadBtn.style.display = 'none';
693
+ const reporterCode = exportsProductCountry.value;
694
+ const year = document.getElementById('exportsProductYear').value;
695
+ // Fetch for all products (HS codes) for this country/year
696
+ const payload = {
697
+ reporterCode: reporterCode,
698
+ partnerCode: '0', // World
699
+ period: year,
700
+ cmdCode: 'ALL', // Get all products
701
+ flowCode: 'X'
702
+ };
703
+ exportsProductResults.innerHTML = '<div>Loading data for all products...</div>';
704
+ showSpinner();
705
+ try {
706
+ const resp = await fetch('/api/trade', {
707
+ method: 'POST',
708
+ headers: { 'Content-Type': 'application/json' },
709
+ body: JSON.stringify(payload)
710
+ });
711
+ const data = await resp.json();
712
+ if (data && data.rows && data.rows.length > 0) {
713
+ // Find the product/HS code column
714
+ const hsCol = data.columns.includes('cmdCode') ? 'cmdCode' : (data.columns.includes('productCode') ? 'productCode' : null);
715
+ const descCol = data.columns.includes('cmdDescE') ? 'cmdDescE' : (data.columns.includes('productDesc') ? 'productDesc' : null);
716
+ const valueCol = data.columns.includes('primaryValue') ? 'primaryValue' : (data.columns.includes('TradeValue') ? 'TradeValue' : (data.columns.includes('Value') ? 'Value' : null));
717
+ if (!hsCol || !valueCol) {
718
+ exportsProductResults.innerHTML = '<div>No product/value columns found.</div>';
719
+ return;
720
+ }
721
+ // Sort by value descending
722
+ const sorted = data.rows.slice().sort((a, b) => b[valueCol] - a[valueCol]);
723
+ exportsProductTableData = sorted;
724
+ // Render table
725
+ let html = '<div style="overflow-x:auto;"><table><thead><tr><th>HS Code</th>';
726
+ if (descCol) html += '<th>Description</th>';
727
+ html += '<th>Value</th></tr></thead><tbody>';
728
+ sorted.forEach(row => {
729
+ html += `<tr><td>${row[hsCol]}</td>`;
730
+ if (descCol) html += `<td>${row[descCol]}</td>`;
731
+ html += `<td>${row[valueCol]}</td></tr>`;
732
+ });
733
+ html += '</tbody></table></div>';
734
+ exportsProductResults.innerHTML = html;
735
+ // Modern chart for Exports by Product
736
+ renderModernChart(sorted, 'exportsProductChart');
737
+ if (sorted.length > 0) exportsProductDownloadBtn.style.display = 'inline-block';
738
+ else exportsProductDownloadBtn.style.display = 'none';
739
+ } else {
740
+ exportsProductResults.innerHTML = '<div>No data found for this country/year.</div>';
741
+ exportsProductDownloadBtn.style.display = 'none';
742
+ }
743
+ } catch (err) {
744
+ exportsProductResults.innerHTML = '<div>Error fetching data.</div>';
745
+ } finally {
746
+ hideSpinner();
747
+ }
748
+ });
749
+ exportsProductDownloadBtn.addEventListener('click', function() {
750
+ if (!exportsProductTableData) return;
751
+ let csv = 'HS Code,Description,Value\n';
752
+ exportsProductTableData.forEach(row => {
753
+ csv += `${row.cmdCode || row.productCode || ''},${row.cmdDescE || row.productDesc || ''},${row.primaryValue || row.TradeValue || row.Value || ''}\n`;
754
+ });
755
+ const blob = new Blob([csv], {type: 'text/csv'});
756
+ const url = URL.createObjectURL(blob);
757
+ const a = document.createElement('a');
758
+ a.href = url;
759
+ a.download = 'exports_by_product.csv';
760
+ document.body.appendChild(a);
761
+ a.click();
762
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
763
+ });
764
+ }
765
+
766
+ const importsCountryForm = document.getElementById('importsCountryForm');
767
+ const importsCountryResults = document.getElementById('importsCountryResults');
768
+ const importsCountryDownloadBtn = document.getElementById('importsCountryDownloadBtn');
769
+ let importsCountryTableData = null;
770
+ if (importsCountryForm) {
771
+ importsCountryForm.addEventListener('submit', async function(e) {
772
+ e.preventDefault();
773
+ importsCountryResults.innerHTML = '';
774
+ importsCountryDownloadBtn.style.display = 'none';
775
+ const year = document.getElementById('importsCountryYear').value;
776
+ const cmdCode = document.getElementById('importsCountryCommodity').value;
777
+ const flowCode = document.getElementById('importsCountryFlow').value;
778
+ // Fetch for all countries: iterate COUNTRY_CODES
779
+ const allPromises = COUNTRY_CODES.map(async country => {
780
+ const payload = {
781
+ reporterCode: country.code,
782
+ partnerCode: '0', // World
783
+ period: year,
784
+ cmdCode: cmdCode,
785
+ flowCode: flowCode
786
+ };
787
+ try {
788
+ const resp = await fetch('/api/trade', {
789
+ method: 'POST',
790
+ headers: { 'Content-Type': 'application/json' },
791
+ body: JSON.stringify(payload)
792
+ });
793
+ const data = await resp.json();
794
+ if (data && data.rows && data.rows.length > 0) {
795
+ // Find the value column (primaryValue or TradeValue or Value)
796
+ const valueCol = data.columns.includes('primaryValue') ? 'primaryValue' : (data.columns.includes('TradeValue') ? 'TradeValue' : (data.columns.includes('Value') ? 'Value' : null));
797
+ const val = valueCol ? data.rows[0][valueCol] : null;
798
+ return {
799
+ country: country.name,
800
+ code: country.code,
801
+ value: val
802
+ };
803
+ } else {
804
+ return {
805
+ country: country.name,
806
+ code: country.code,
807
+ value: null
808
+ };
809
+ }
810
+ } catch (err) {
811
+ return {
812
+ country: country.name,
813
+ code: country.code,
814
+ value: null
815
+ };
816
+ }
817
+ });
818
+ importsCountryResults.innerHTML = '<div>Loading data for all countries...</div>';
819
+ const allResults = await Promise.all(allPromises);
820
+ // Filter for non-null values and sort descending
821
+ const filtered = allResults.filter(r => r.value !== null && r.value !== undefined).sort((a, b) => b.value - a.value);
822
+ importsCountryTableData = filtered;
823
+ // Render table
824
+ let html = '<div style="overflow-x:auto;"><table><thead><tr><th>Country</th><th>Code</th><th>Value</th></tr></thead><tbody>';
825
+ filtered.forEach(row => {
826
+ html += `<tr><td>${row.country}</td><td>${row.code}</td><td>${row.value}</td></tr>`;
827
+ });
828
+ html += '</tbody></table></div>';
829
+ importsCountryResults.innerHTML = html;
830
+ if (filtered.length > 0) importsCountryDownloadBtn.style.display = 'inline-block';
831
+ else importsCountryDownloadBtn.style.display = 'none';
832
+ // Modern chart for Imports by Country
833
+ renderModernChart(filtered, 'importsCountryChart');
834
+ });
835
+ importsCountryDownloadBtn.addEventListener('click', function() {
836
+ if (!importsCountryTableData) return;
837
+ let csv = 'Country,Code,Value\n';
838
+ importsCountryTableData.forEach(row => {
839
+ csv += `${row.country},${row.code},${row.value}\n`;
840
+ });
841
+ const blob = new Blob([csv], {type: 'text/csv'});
842
+ const url = URL.createObjectURL(blob);
843
+ const a = document.createElement('a');
844
+ a.href = url;
845
+ a.download = 'imports_by_country.csv';
846
+ document.body.appendChild(a);
847
+ a.click();
848
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
849
+ });
850
+ }
851
+
852
+ const exportsCountryForm = document.getElementById('exportsCountryForm');
853
+ const exportsCountryResults = document.getElementById('exportsCountryResults');
854
+ const exportsCountryDownloadBtn = document.getElementById('exportsCountryDownloadBtn');
855
+ let exportsCountryTableData = null;
856
+ if (exportsCountryForm) {
857
+ exportsCountryForm.addEventListener('submit', async function(e) {
858
+ e.preventDefault();
859
+ exportsCountryResults.innerHTML = '';
860
+ exportsCountryDownloadBtn.style.display = 'none';
861
+ const year = document.getElementById('exportsCountryYear').value;
862
+ const cmdCode = document.getElementById('exportsCountryCommodity').value;
863
+ const flowCode = document.getElementById('exportsCountryFlow').value;
864
+ // Fetch for all countries: iterate COUNTRY_CODES
865
+ const allPromises = COUNTRY_CODES.map(async country => {
866
+ const payload = {
867
+ reporterCode: country.code,
868
+ partnerCode: '0', // World
869
+ period: year,
870
+ cmdCode: cmdCode,
871
+ flowCode: flowCode
872
+ };
873
+ try {
874
+ const resp = await fetch('/api/trade', {
875
+ method: 'POST',
876
+ headers: { 'Content-Type': 'application/json' },
877
+ body: JSON.stringify(payload)
878
+ });
879
+ const data = await resp.json();
880
+ if (data && data.rows && data.rows.length > 0) {
881
+ // Find the value column (primaryValue or TradeValue or Value)
882
+ const valueCol = data.columns.includes('primaryValue') ? 'primaryValue' : (data.columns.includes('TradeValue') ? 'TradeValue' : (data.columns.includes('Value') ? 'Value' : null));
883
+ const val = valueCol ? data.rows[0][valueCol] : null;
884
+ return {
885
+ country: country.name,
886
+ code: country.code,
887
+ value: val
888
+ };
889
+ } else {
890
+ return {
891
+ country: country.name,
892
+ code: country.code,
893
+ value: null
894
+ };
895
+ }
896
+ } catch (err) {
897
+ return {
898
+ country: country.name,
899
+ code: country.code,
900
+ value: null
901
+ };
902
+ }
903
+ });
904
+ exportsCountryResults.innerHTML = '<div>Loading data for all countries...</div>';
905
+ const allResults = await Promise.all(allPromises);
906
+ // Filter for non-null values and sort descending
907
+ const filtered = allResults.filter(r => r.value !== null && r.value !== undefined).sort((a, b) => b.value - a.value);
908
+ exportsCountryTableData = filtered;
909
+ // Render table
910
+ let html = '<div style="overflow-x:auto;"><table><thead><tr><th>Country</th><th>Code</th><th>Value</th></tr></thead><tbody>';
911
+ filtered.forEach(row => {
912
+ html += `<tr><td>${row.country}</td><td>${row.code}</td><td>${row.value}</td></tr>`;
913
+ });
914
+ html += '</tbody></table></div>';
915
+ exportsCountryResults.innerHTML = html;
916
+ if (filtered.length > 0) exportsCountryDownloadBtn.style.display = 'inline-block';
917
+ else exportsCountryDownloadBtn.style.display = 'none';
918
+ // Modern chart for Exports by Country
919
+ renderModernChart(filtered, 'exportsCountryChart');
920
+ });
921
+ exportsCountryDownloadBtn.addEventListener('click', function() {
922
+ if (!exportsCountryTableData) return;
923
+ let csv = 'Country,Code,Value\n';
924
+ exportsCountryTableData.forEach(row => {
925
+ csv += `${row.country},${row.code},${row.value}\n`;
926
+ });
927
+ const blob = new Blob([csv], {type: 'text/csv'});
928
+ const url = URL.createObjectURL(blob);
929
+ const a = document.createElement('a');
930
+ a.href = url;
931
+ a.download = 'exports_by_country.csv';
932
+ document.body.appendChild(a);
933
+ a.click();
934
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
935
+ });
936
+ }
937
+
938
+
939
+ const form = document.getElementById('tradeForm');
940
+ const resultsDiv = document.getElementById('results');
941
+ const predictForm = document.getElementById('predictForm');
942
+ const predictionResult = document.getElementById('predictionResult');
943
+ const alertDiv = document.getElementById('alert');
944
+ const spinner = document.getElementById('spinner');
945
+ const tradeChart = document.getElementById('tradeChart');
946
+ let chartInstance = null;
947
+
948
+ function showAlert(msg, type='danger') {
949
+ alertDiv.style.display = 'block';
950
+ alertDiv.className = 'alert ' + (type === 'success' ? 'alert-success' : 'alert-danger');
951
+ alertDiv.textContent = msg;
952
+ }
953
+ function clearAlert() {
954
+ alertDiv.style.display = 'none';
955
+ alertDiv.textContent = '';
956
+ }
957
+ function showSpinner() { spinner.style.display = 'block'; }
958
+ function hideSpinner() { spinner.style.display = 'none'; }
959
+ function renderTable(columns, rows) {
960
+ let html = '<div style="overflow-x:auto;"><table><thead><tr>';
961
+ columns.forEach(col => html += `<th>${col}</th>`);
962
+ html += '</tr></thead><tbody>';
963
+ rows.forEach(row => {
964
+ html += '<tr>';
965
+ columns.forEach(col => html += `<td>${row[col]}</td>`);
966
+ html += '</tr>';
967
+ });
968
+ html += '</tbody></table></div>';
969
+ return html;
970
+ }
971
+ function renderChart(columns, rows) {
972
+ if (!tradeChart) return;
973
+ // Try to plot year vs primaryValue
974
+ const yearCol = columns.includes('year') ? 'year' : (columns.includes('refYear') ? 'refYear' : null);
975
+ const valueCol = columns.includes('primaryValue') ? 'primaryValue' : null;
976
+ if (!yearCol || !valueCol) { tradeChart.style.display = 'none'; return; }
977
+ const dataByYear = {};
978
+ rows.forEach(row => {
979
+ const y = row[yearCol] || row['refYear'];
980
+ const v = row[valueCol];
981
+ if (y && v) dataByYear[y] = v;
982
+ });
983
+ const years = Object.keys(dataByYear).sort();
984
+ const values = years.map(y => dataByYear[y]);
985
+ if (chartInstance) chartInstance.destroy();
986
+ chartInstance = new window.Chart(tradeChart.getContext('2d'), {
987
+ type: 'line',
988
+ data: {
989
+ labels: years,
990
+ datasets: [{
991
+ label: 'Trade Value',
992
+ data: values,
993
+ borderColor: '#3498db',
994
+ backgroundColor: 'rgba(52,152,219,0.2)',
995
+ fill: true
996
+ }]
997
+ },
998
+ options: { responsive: true, plugins: { legend: { display: false } } }
999
+ });
1000
+ tradeChart.style.display = 'block';
1001
+ }
1002
+
1003
+ // Initialize select2 on country dropdowns (after country list loads)
1004
+
1005
+ // --- World map visualization ---
1006
+ let map = null;
1007
+ let reporterMarker = null;
1008
+ let partnerMarker = null;
1009
+ let countryLatLng = {
1010
+ '842': [38.0, -97.0], // USA
1011
+ '156': [35.0, 103.0], // China
1012
+ '392': [36.2, 138.2], // Japan
1013
+ '826': [54.0, -2.0], // UK
1014
+ '124': [56.1, -106.3], // Canada
1015
+ '250': [46.6, 2.2], // France
1016
+ '276': [51.2, 10.4], // Germany
1017
+ '380': [41.9, 12.5], // Italy
1018
+ '484': [23.6, -102.5], // Mexico
1019
+ '356': [20.6, 78.9], // India
1020
+ '643': [61.5, 105.3], // Russia
1021
+ '710': [-30.6, 22.9], // South Africa
1022
+ '036': [-25.3, 133.8], // Australia
1023
+ '410': [36.5, 127.9], // South Korea
1024
+ '704': [14.1, 108.3], // Vietnam
1025
+ '458': [4.2, 101.9], // Malaysia
1026
+ '554': [-40.9, 174.9], // New Zealand
1027
+ '764': [15.8, 100.9], // Thailand
1028
+ '344': [22.3, 114.2], // Hong Kong
1029
+ };
1030
+ function updateMap() {
1031
+ if (!window.L || !document.getElementById('worldMap')) return;
1032
+ if (!map) {
1033
+ map = L.map('worldMap').setView([20, 0], 2);
1034
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
1035
+ attribution: 'OpenStreetMap contributors'
1036
+ }).addTo(map);
1037
+ }
1038
+ // Remove old markers
1039
+ if (reporterMarker) { map.removeLayer(reporterMarker); reporterMarker = null; }
1040
+ if (partnerMarker) { map.removeLayer(partnerMarker); partnerMarker = null; }
1041
+ // Add new markers
1042
+ const reporterCode = document.getElementById('reporterCode').value;
1043
+ const partnerCode = document.getElementById('partnerCode').value;
1044
+ if (countryLatLng[reporterCode]) {
1045
+ reporterMarker = L.marker(countryLatLng[reporterCode], {icon: L.icon({iconUrl:'https://cdn.jsdelivr.net/npm/[email protected]/dist/images/marker-icon.png',iconSize:[25,41],iconAnchor:[12,41],popupAnchor:[1,-34],shadowUrl:'https://cdn.jsdelivr.net/npm/[email protected]/dist/images/marker-shadow.png',shadowSize:[41,41]})}).addTo(map).bindPopup('Reporter Country');
1046
+ }
1047
+ if (countryLatLng[partnerCode]) {
1048
+ partnerMarker = L.marker(countryLatLng[partnerCode], {icon: L.icon({iconUrl:'https://cdn.jsdelivr.net/npm/[email protected]/dist/images/marker-icon-red.png',iconSize:[25,41],iconAnchor:[12,41],popupAnchor:[1,-34],shadowUrl:'https://cdn.jsdelivr.net/npm/[email protected]/dist/images/marker-shadow.png',shadowSize:[41,41]})}).addTo(map).bindPopup('Partner Country');
1049
+ }
1050
+ }
1051
+ document.getElementById('reporterCode').addEventListener('change', updateMap);
1052
+ document.getElementById('partnerCode').addEventListener('change', updateMap);
1053
+ setTimeout(updateMap, 1000); // Initial update after map loads
1054
+
1055
+ // Placeholder for extra visualizations
1056
+ function showExtraVisualizations(data) {
1057
+ const div = document.getElementById('extraVisualizations');
1058
+ div.innerHTML = '<h3>Additional Visualizations (coming soon)</h3>';
1059
+ }
1060
+
1061
+ clearAlert();
1062
+ form.addEventListener('submit', async function(e) {
1063
+ e.preventDefault();
1064
+ clearAlert();
1065
+ resultsDiv.innerHTML = '';
1066
+ tradeChart.style.display = 'none';
1067
+ showSpinner();
1068
+ const formData = new FormData(form);
1069
+ const data = {};
1070
+ formData.forEach((value, key) => {
1071
+ if (value !== '') data[key] = value;
1072
+ });
1073
+ try {
1074
+ const response = await fetch('/api/trade', {
1075
+ method: 'POST',
1076
+ headers: {'Content-Type': 'application/json'},
1077
+ body: JSON.stringify(data)
1078
+ });
1079
+ const json = await response.json();
1080
+ hideSpinner();
1081
+ if (json.error) {
1082
+ showAlert('Error: ' + json.error, 'danger');
1083
+ resultsDiv.innerHTML = '';
1084
+ } else if (!json.rows || json.rows.length === 0) {
1085
+ showAlert('No data returned for these parameters.', 'danger');
1086
+ resultsDiv.innerHTML = '';
1087
+ } else {
1088
+ showAlert('Data loaded successfully!', 'success');
1089
+ resultsDiv.innerHTML = renderTable(json.columns, json.rows);
1090
+ renderChart(json.columns, json.rows);
1091
+ }
1092
+ } catch (err) {
1093
+ showAlert('Request failed: ' + err, 'danger');
1094
+ resultsDiv.innerHTML = '';
1095
+ } finally {
1096
+ hideSpinner();
1097
+ }
1098
+ });
1099
+
1100
+ predictForm.addEventListener('submit', async function(e) {
1101
+ e.preventDefault();
1102
+ clearAlert();
1103
+ predictionResult.innerHTML = '<p>Predicting...</p>';
1104
+ // Gather parameters from both forms
1105
+ const formData = new FormData(form);
1106
+ const predictData = new FormData(predictForm);
1107
+ const data = {};
1108
+ formData.forEach((value, key) => { if (value !== '') data[key] = value; });
1109
+ predictData.forEach((value, key) => { if (value !== '') data[key] = value; });
1110
+ showSpinner();
1111
+ try {
1112
+ const response = await fetch('/api/predict', {
1113
+ method: 'POST',
1114
+ headers: {'Content-Type': 'application/json'},
1115
+ body: JSON.stringify(data)
1116
+ });
1117
+
1118
+ if (!response.ok) {
1119
+ throw new Error(`HTTP error! Status: ${response.status}`);
1120
+ }
1121
+
1122
+ const json = await response.json();
1123
+
1124
+ if (json.error) {
1125
+ showAlert('Prediction error: ' + json.error, 'danger');
1126
+ predictionResult.innerHTML = '';
1127
+ } else if (json.prediction !== undefined) {
1128
+ showAlert('Prediction successful! Model: ' + (json.model_type || 'N/A'), 'success');
1129
+
1130
+ // Format the prediction result
1131
+ let predictionValue = typeof json.prediction === 'number' ?
1132
+ json.prediction.toLocaleString(undefined, {maximumFractionDigits:2}) :
1133
+ json.prediction;
1134
+
1135
+ // Get MSE value safely
1136
+ let mseValue = json.mse !== undefined ?
1137
+ (typeof json.mse === 'number' ? json.mse.toLocaleString(undefined, {maximumFractionDigits:2}) : json.mse) :
1138
+ 'N/A';
1139
+
1140
+ predictionResult.innerHTML = `<p><strong>Predicted Trade Value:</strong> ${predictionValue}<br><small>(Model: ${json.model_type || ''} | MSE: ${mseValue})</small></p>`;
1141
+
1142
+ // If we have historical data, plot a chart
1143
+ if (json.historical && Array.isArray(json.historical)) {
1144
+ // Prepare table data for chart
1145
+ let rows = [];
1146
+ json.historical.forEach(row => {
1147
+ rows.push({ year: row.year, value: row.value, type: 'historical' });
1148
+ });
1149
+ rows.push({ year: json.prediction_year || predict_year, value: json.prediction, type: 'predicted' });
1150
+
1151
+ // Save for export
1152
+ predictionTableData = rows;
1153
+
1154
+ // Display the download button
1155
+ predictionDownloadBtn.style.display = 'inline-block';
1156
+
1157
+ // Plot the chart
1158
+ plotPredictionChart(rows);
1159
+ }
1160
+ } else {
1161
+ showAlert('No prediction data returned', 'danger');
1162
+ predictionResult.innerHTML = '<div>No prediction data returned.</div>';
1163
+ }
1164
+ } catch (err) {
1165
+ showAlert('Prediction failed: ' + err, 'danger');
1166
+ predictionResult.innerHTML = '';
1167
+ } finally {
1168
+ hideSpinner();
1169
+ }
1170
+ });
1171
+
1172
+ // Load Chart.js dynamically if not present
1173
+ if (!window.Chart) {
1174
+ const script = document.createElement('script');
1175
+ script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
1176
+ document.body.appendChild(script);
1177
+ }
1178
+
1179
+ // ---- Tab Navigation Logic ----
1180
+ const tabs = document.querySelectorAll('#main-tabs .tab');
1181
+ const tabContents = document.querySelectorAll('.tab-content');
1182
+ tabs.forEach((tab, idx) => {
1183
+ tab.addEventListener('click', function() {
1184
+ // Hide all spinners in all tab panels
1185
+ document.querySelectorAll('.spinner').forEach(spinner => spinner.style.display = 'none');
1186
+ hideSpinner(); // Also hide main spinner for good measure
1187
+ tabs.forEach((t, i) => {
1188
+ t.classList.remove('active');
1189
+ t.setAttribute('aria-selected', 'false');
1190
+ });
1191
+ tab.classList.add('active');
1192
+ tab.setAttribute('aria-selected', 'true');
1193
+ const tabName = tab.getAttribute('data-tab');
1194
+ tabContents.forEach(panel => {
1195
+ if (panel.id === 'tab-content-' + tabName) {
1196
+ panel.style.display = 'block';
1197
+ } else {
1198
+ panel.style.display = 'none';
1199
+ }
1200
+ });
1201
+ tab.focus();
1202
+ });
1203
+ tab.addEventListener('keydown', function(e) {
1204
+ if (e.key === 'ArrowRight') {
1205
+ e.preventDefault();
1206
+ tabs[(idx + 1) % tabs.length].focus();
1207
+ } else if (e.key === 'ArrowLeft') {
1208
+ e.preventDefault();
1209
+ tabs[(idx - 1 + tabs.length) % tabs.length].focus();
1210
+ } else if (e.key === 'Enter' || e.key === ' ') {
1211
+ e.preventDefault();
1212
+ tab.click();
1213
+ }
1214
+ });
1215
+ });
1216
+ });