Spaces:
Running
Running
Upload static/js/main.js with huggingface_hub
Browse files- 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 |
+
});
|