Spaces:
Running
Running
Upload index.html with huggingface_hub
Browse files- index.html +1439 -19
index.html
CHANGED
@@ -1,19 +1,1439 @@
|
|
1 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en" data-theme="light">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
6 |
+
<title>Vietnam Economic Growth Report 2025 — Interactive Research Dashboard</title>
|
7 |
+
<meta name="description" content="Comprehensive analytical dashboard for Vietnam's 2025 economic performance: GDP, inflation, unemployment, FDI, sectoral analysis, forecasts, and citations." />
|
8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
9 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
10 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
11 |
+
<style>
|
12 |
+
:root {
|
13 |
+
--bg: #0b0f14;
|
14 |
+
--panel: #11161d;
|
15 |
+
--elev: #0f141b;
|
16 |
+
--text: #eaf2ff;
|
17 |
+
--muted: #a8b2c2;
|
18 |
+
--accent: #3ab5ff;
|
19 |
+
--accent-2: #00ffa3;
|
20 |
+
--accent-3: #ff7a59;
|
21 |
+
--bdr: rgba(255,255,255,0.08);
|
22 |
+
--ok: #2ecc71;
|
23 |
+
--warn: #f1c40f;
|
24 |
+
--err: #e74c3c;
|
25 |
+
--shadow: 0 10px 30px rgba(0,0,0,0.35), 0 1px 0 rgba(255,255,255,0.02) inset;
|
26 |
+
--radius: 14px;
|
27 |
+
--radius-sm: 10px;
|
28 |
+
--radius-lg: 22px;
|
29 |
+
--space-1: 0.35rem;
|
30 |
+
--space-2: 0.6rem;
|
31 |
+
--space-3: 0.9rem;
|
32 |
+
--space-4: 1.25rem;
|
33 |
+
--space-5: 2rem;
|
34 |
+
--maxw: 1400px;
|
35 |
+
--toc-w: 280px;
|
36 |
+
--font-size: clamp(15px, 1.25vw, 17px);
|
37 |
+
--h1: clamp(1.8rem, 5vw, 3rem);
|
38 |
+
--h2: clamp(1.3rem, 3.2vw, 2rem);
|
39 |
+
--h3: clamp(1.15rem, 2.2vw, 1.4rem);
|
40 |
+
--kbd-bg: #0a0d12;
|
41 |
+
--link: #7cc8ff;
|
42 |
+
--mark: rgba(255, 235, 59, .25);
|
43 |
+
--chip: #1a2330;
|
44 |
+
}
|
45 |
+
[data-theme="light"] {
|
46 |
+
--bg: #f6f8fb;
|
47 |
+
--panel: #ffffff;
|
48 |
+
--elev: #ffffff;
|
49 |
+
--text: #0f1726;
|
50 |
+
--muted: #475569;
|
51 |
+
--accent: #0066ff;
|
52 |
+
--accent-2: #10b981;
|
53 |
+
--accent-3: #ff5a3c;
|
54 |
+
--bdr: rgba(0,0,0,0.08);
|
55 |
+
--shadow: 0 8px 28px rgba(12, 17, 29, 0.07), 0 1px 0 rgba(0,0,0,0.03) inset;
|
56 |
+
--kbd-bg: #ecf1f8;
|
57 |
+
--link: #0056d6;
|
58 |
+
--chip: #eef4ff;
|
59 |
+
}
|
60 |
+
|
61 |
+
* { box-sizing: border-box; }
|
62 |
+
html { scroll-behavior: smooth; }
|
63 |
+
body {
|
64 |
+
margin: 0; font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
65 |
+
background: radial-gradient(1200px 600px at 10% -10%, rgba(58,181,255,0.15), transparent 40%),
|
66 |
+
radial-gradient(900px 500px at 110% 10%, rgba(0,255,163,0.12), transparent 40%),
|
67 |
+
var(--bg);
|
68 |
+
color: var(--text); font-size: var(--font-size); line-height: 1.5;
|
69 |
+
}
|
70 |
+
a { color: var(--link); text-decoration: none; }
|
71 |
+
a:hover { text-decoration: underline; }
|
72 |
+
kbd {
|
73 |
+
background: var(--kbd-bg); border: 1px solid var(--bdr);
|
74 |
+
border-bottom-width: 2px; border-radius: 6px; padding: 0 .35rem; font-size: .85em;
|
75 |
+
box-shadow: var(--shadow);
|
76 |
+
}
|
77 |
+
.wrap {
|
78 |
+
display: grid;
|
79 |
+
grid-template-columns: 1fr;
|
80 |
+
gap: var(--space-5);
|
81 |
+
max-width: var(--maxw);
|
82 |
+
margin: 0 auto;
|
83 |
+
padding: clamp(10px, 1.8vw, 22px);
|
84 |
+
}
|
85 |
+
header.app {
|
86 |
+
position: sticky; top: 0; z-index: 50; backdrop-filter: saturate(1.2) blur(10px);
|
87 |
+
background: color-mix(in hsl, var(--bg) 70%, transparent);
|
88 |
+
border-bottom: 1px solid var(--bdr);
|
89 |
+
}
|
90 |
+
.topbar {
|
91 |
+
max-width: var(--maxw);
|
92 |
+
margin: 0 auto; padding: .6rem clamp(10px, 1.8vw, 22px); display: flex; gap: .75rem; align-items: center; justify-content: space-between;
|
93 |
+
}
|
94 |
+
.brand { display: flex; align-items: center; gap: .75rem; font-weight: 800; letter-spacing: .2px; }
|
95 |
+
.brand .logo {
|
96 |
+
width: 36px; height: 36px; display: grid; place-items: center; border-radius: 10px;
|
97 |
+
background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: white;
|
98 |
+
box-shadow: 0 10px 20px color-mix(in oklab, var(--accent) 25%, transparent);
|
99 |
+
}
|
100 |
+
.toolbar { display: flex; align-items: center; gap: .5rem; }
|
101 |
+
.searchbar {
|
102 |
+
display: flex; align-items: center; gap: .5rem; background: var(--panel);
|
103 |
+
border: 1px solid var(--bdr); border-radius: 999px; padding: .35rem .65rem; width: min(540px, 55vw);
|
104 |
+
box-shadow: var(--shadow);
|
105 |
+
}
|
106 |
+
.searchbar input {
|
107 |
+
border: none; outline: none; background: transparent; width: 100%; color: var(--text);
|
108 |
+
font-size: .95rem;
|
109 |
+
}
|
110 |
+
.btn {
|
111 |
+
display: inline-flex; align-items: center; gap: .5rem; border: 1px solid var(--bdr);
|
112 |
+
background: var(--panel); color: var(--text); padding: .45rem .75rem; border-radius: 10px; cursor: pointer;
|
113 |
+
box-shadow: var(--shadow);
|
114 |
+
}
|
115 |
+
.btn.primary { background: linear-gradient(180deg, color-mix(in oklab, var(--accent) 18%, transparent), transparent), var(--panel); border-color: color-mix(in oklab, var(--accent) 35%, var(--bdr)); }
|
116 |
+
.btn.ghost { background: transparent; }
|
117 |
+
.btn:active { transform: translateY(1px); }
|
118 |
+
.progressbar {
|
119 |
+
position: absolute; left: 0; bottom: 0; height: 3px; width: 0%;
|
120 |
+
background: linear-gradient(90deg, var(--accent), var(--accent-2)); transition: width .2s ease;
|
121 |
+
}
|
122 |
+
|
123 |
+
.layout {
|
124 |
+
display: grid; grid-template-columns: 1fr; gap: var(--space-5);
|
125 |
+
}
|
126 |
+
aside.toc {
|
127 |
+
position: sticky; top: 70px; align-self: start; border: 1px solid var(--bdr);
|
128 |
+
background: var(--panel); border-radius: var(--radius); padding: var(--space-4);
|
129 |
+
box-shadow: var(--shadow);
|
130 |
+
max-height: calc(100vh - 90px); overflow: auto;
|
131 |
+
}
|
132 |
+
.toc h4 { margin: 0 0 .5rem 0; font-size: .95rem; color: var(--muted); display: flex; align-items: center; gap: .4rem; }
|
133 |
+
.toc nav a {
|
134 |
+
display: block; padding: .4rem .5rem; margin: .1rem 0; border-radius: 8px; color: var(--text);
|
135 |
+
text-decoration: none; border: 1px solid transparent;
|
136 |
+
}
|
137 |
+
.toc nav a:hover { background: color-mix(in oklab, var(--accent) 16%, transparent); }
|
138 |
+
.toc nav a.active {
|
139 |
+
border-color: color-mix(in oklab, var(--accent) 40%, transparent);
|
140 |
+
background: color-mix(in oklab, var(--accent) 10%, transparent);
|
141 |
+
}
|
142 |
+
|
143 |
+
.main {
|
144 |
+
display: grid; gap: var(--space-5);
|
145 |
+
}
|
146 |
+
|
147 |
+
.hero {
|
148 |
+
display: grid; gap: var(--space-4); border: 1px solid var(--bdr);
|
149 |
+
background: linear-gradient(160deg, color-mix(in oklab, var(--accent) 6%, transparent), transparent 60%), var(--panel);
|
150 |
+
border-radius: var(--radius-lg); padding: clamp(14px, 2.2vw, 30px); box-shadow: var(--shadow);
|
151 |
+
}
|
152 |
+
.hero h1 { font-size: var(--h1); line-height: 1.1; margin: 0; letter-spacing: -0.02em; }
|
153 |
+
.subhead { color: var(--muted); font-weight: 500; }
|
154 |
+
|
155 |
+
.kpi-grid {
|
156 |
+
display: grid; gap: var(--space-4);
|
157 |
+
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
|
158 |
+
container-type: inline-size; container-name: kpi;
|
159 |
+
}
|
160 |
+
.kpi {
|
161 |
+
background: var(--elev); border: 1px solid var(--bdr); border-radius: 14px; padding: var(--space-4);
|
162 |
+
display: grid; gap: .5rem; position: relative; overflow: clip; box-shadow: var(--shadow);
|
163 |
+
}
|
164 |
+
.kpi .value { font-size: clamp(1.4rem, 4vw, 2rem); font-weight: 800; letter-spacing: -0.02em; }
|
165 |
+
.kpi .label { color: var(--muted); font-weight: 600; font-size: .9rem; }
|
166 |
+
.kpi .delta { font-size: .85rem; display: inline-flex; align-items: center; gap: .3rem; border-radius: 999px; padding: .15rem .5rem; background: color-mix(in oklab, var(--accent-2) 12%, transparent); color: var(--accent-2); }
|
167 |
+
.kpi .actions { margin-top: .35rem; display: flex; gap: .35rem; flex-wrap: wrap; }
|
168 |
+
.kpi .bg {
|
169 |
+
pointer-events: none; content: ""; position: absolute; inset: auto -20% -35% auto; width: 140px; height: 140px;
|
170 |
+
background: radial-gradient(60% 60% at 50% 50%, color-mix(in oklab, var(--accent) 35%, transparent), transparent);
|
171 |
+
border-radius: 50%;
|
172 |
+
opacity: .35;
|
173 |
+
}
|
174 |
+
@container kpi (min-width: 240px) {
|
175 |
+
.kpi .value { font-size: clamp(1.6rem, 3.4cqi, 2.2rem); }
|
176 |
+
}
|
177 |
+
|
178 |
+
.section {
|
179 |
+
border: 1px solid var(--bdr); background: var(--panel); border-radius: var(--radius);
|
180 |
+
padding: clamp(14px, 2vw, 26px); box-shadow: var(--shadow);
|
181 |
+
}
|
182 |
+
.section h2 { font-size: var(--h2); line-height: 1.15; margin: 0 0 .5rem 0; scroll-margin-top: 80px; }
|
183 |
+
.section .summary {
|
184 |
+
background: color-mix(in oklab, var(--accent) 10%, transparent); border: 1px dashed color-mix(in oklab, var(--accent) 45%, var(--bdr));
|
185 |
+
padding: .75rem .9rem; border-radius: 12px; color: var(--text);
|
186 |
+
}
|
187 |
+
.section .chips { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .5rem; }
|
188 |
+
.chip {
|
189 |
+
background: var(--chip); color: var(--text); padding: .35rem .6rem; border-radius: 999px; border: 1px solid var(--bdr);
|
190 |
+
font-size: .85rem; display: inline-flex; align-items: center; gap: .35rem;
|
191 |
+
}
|
192 |
+
.with-toolbar { display: grid; gap: var(--space-3); }
|
193 |
+
.toolbar-row { display: flex; align-items: center; justify-content: space-between; gap: .75rem; flex-wrap: wrap; }
|
194 |
+
.tools { display: flex; gap: .5rem; flex-wrap: wrap; }
|
195 |
+
|
196 |
+
.grid {
|
197 |
+
display: grid; gap: var(--space-4);
|
198 |
+
grid-template-columns: 1fr;
|
199 |
+
}
|
200 |
+
@media (min-width: 768px) {
|
201 |
+
.layout { grid-template-columns: minmax(0, 1fr) 300px; }
|
202 |
+
.grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
203 |
+
.grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
204 |
+
}
|
205 |
+
@media (min-width: 1024px) {
|
206 |
+
.layout { grid-template-columns: 280px minmax(0, 1fr); column-gap: var(--space-5); }
|
207 |
+
.grid.cols-3-xl { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
208 |
+
}
|
209 |
+
@media (min-width: 1440px) {
|
210 |
+
.layout { grid-template-columns: var(--toc-w) minmax(0, 1fr); }
|
211 |
+
}
|
212 |
+
|
213 |
+
.chart-card {
|
214 |
+
background: var(--elev); border: 1px solid var(--bdr); border-radius: var(--radius); padding: 1rem;
|
215 |
+
display: grid; gap: .5rem; box-shadow: var(--shadow);
|
216 |
+
container-type: inline-size; container-name: chart;
|
217 |
+
}
|
218 |
+
.chart-card h3 { margin: 0; font-size: var(--h3); }
|
219 |
+
.chart {
|
220 |
+
width: 100%; aspect-ratio: 16 / 9; background:
|
221 |
+
linear-gradient(0deg, transparent 24%, color-mix(in oklab, var(--text) 2%, transparent) 25% 26%, transparent 27%),
|
222 |
+
linear-gradient(90deg, transparent 24%, color-mix(in oklab, var(--text) 2%, transparent) 25% 26%, transparent 27%);
|
223 |
+
background-size: 40px 40px;
|
224 |
+
border-radius: 12px; border: 1px solid var(--bdr); display: grid; place-items: center; position: relative; overflow: hidden;
|
225 |
+
}
|
226 |
+
.legend { display: flex; flex-wrap: wrap; gap: .6rem; font-size: .85rem; color: var(--muted); }
|
227 |
+
.legend .key { display: inline-flex; align-items: center; gap: .35rem; }
|
228 |
+
.legend .sw { width: 10px; height: 10px; border-radius: 50%; }
|
229 |
+
|
230 |
+
.collapsible {
|
231 |
+
border: 1px solid var(--bdr); border-radius: 12px; overflow: hidden;
|
232 |
+
background: var(--elev);
|
233 |
+
}
|
234 |
+
.collapsible summary {
|
235 |
+
list-style: none; cursor: pointer; padding: .75rem 1rem; font-weight: 600;
|
236 |
+
display: flex; align-items: center; justify-content: space-between; gap: .5rem;
|
237 |
+
border-bottom: 1px solid var(--bdr);
|
238 |
+
}
|
239 |
+
.collapsible summary::-webkit-details-marker { display: none; }
|
240 |
+
.collapsible .content { padding: 1rem; }
|
241 |
+
|
242 |
+
.table-wrap { overflow: auto; border: 1px solid var(--bdr); border-radius: 12px; background: var(--elev); }
|
243 |
+
table {
|
244 |
+
width: 100%; border-collapse: collapse; min-width: 680px;
|
245 |
+
}
|
246 |
+
th, td {
|
247 |
+
text-align: left; padding: .7rem .9rem; border-bottom: 1px solid var(--bdr);
|
248 |
+
}
|
249 |
+
th {
|
250 |
+
position: sticky; top: 0; background: linear-gradient(180deg, color-mix(in oklab, var(--panel) 65%, transparent), transparent), var(--elev);
|
251 |
+
font-weight: 700; user-select: none; cursor: pointer;
|
252 |
+
}
|
253 |
+
tr:hover td { background: color-mix(in oklab, var(--accent) 7%, transparent); }
|
254 |
+
|
255 |
+
.notice {
|
256 |
+
background: color-mix(in oklab, var(--warn) 18%, transparent);
|
257 |
+
border: 1px solid color-mix(in oklab, var(--warn) 40%, var(--bdr));
|
258 |
+
padding: .75rem .9rem; border-radius: 12px; color: var(--text);
|
259 |
+
}
|
260 |
+
.info {
|
261 |
+
background: color-mix(in oklab, var(--accent) 12%, transparent);
|
262 |
+
border: 1px solid color-mix(in oklab, var(--accent) 42%, var(--bdr));
|
263 |
+
padding: .75rem .9rem; border-radius: 12px;
|
264 |
+
}
|
265 |
+
|
266 |
+
.foot {
|
267 |
+
display: grid; gap: .75rem; color: var(--muted);
|
268 |
+
border-top: 1px solid var(--bdr); padding: var(--space-4) 0 var(--space-5);
|
269 |
+
}
|
270 |
+
.pill {
|
271 |
+
display: inline-flex; align-items: center; gap: .4rem; padding: .2rem .5rem; border-radius: 999px;
|
272 |
+
background: color-mix(in oklab, var(--accent-2) 12%, transparent);
|
273 |
+
border: 1px solid color-mix(in oklab, var(--accent-2) 40%, var(--bdr));
|
274 |
+
color: var(--text); font-size: .85rem;
|
275 |
+
}
|
276 |
+
|
277 |
+
.meta-row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
|
278 |
+
.small { font-size: .88rem; color: var(--muted); }
|
279 |
+
|
280 |
+
.highlight { background: var(--mark); border-radius: 4px; padding: 0 .15rem; }
|
281 |
+
|
282 |
+
.sticky-tools {
|
283 |
+
position: sticky; bottom: 12px; z-index: 40; display: grid; justify-items: end;
|
284 |
+
}
|
285 |
+
.floater {
|
286 |
+
display: inline-flex; gap: .5rem; padding: .5rem; background: var(--panel); border: 1px solid var(--bdr); border-radius: 14px;
|
287 |
+
box-shadow: var(--shadow);
|
288 |
+
}
|
289 |
+
|
290 |
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
|
291 |
+
|
292 |
+
@media (max-width: 480px) {
|
293 |
+
.searchbar { width: 100%; }
|
294 |
+
.brand .title { display: none; }
|
295 |
+
}
|
296 |
+
</style>
|
297 |
+
</head>
|
298 |
+
<body>
|
299 |
+
<header class="app">
|
300 |
+
<div class="topbar">
|
301 |
+
<div class="brand">
|
302 |
+
<div class="logo" aria-hidden="true"><i data-feather="activity"></i></div>
|
303 |
+
<div class="title">Vietnam Economic Growth Report 2025</div>
|
304 |
+
</div>
|
305 |
+
<div class="toolbar">
|
306 |
+
<div class="searchbar" role="search">
|
307 |
+
<i data-feather="search" aria-hidden="true"></i>
|
308 |
+
<input id="globalSearch" type="search" placeholder="Search the report (Press / to focus)" aria-label="Search" />
|
309 |
+
<button class="btn ghost" id="clearSearch" title="Clear search"><i data-feather="x-circle"></i></button>
|
310 |
+
</div>
|
311 |
+
<button class="btn" id="themeToggle" title="Toggle theme">
|
312 |
+
<i data-feather="moon"></i>
|
313 |
+
<span class="hide@sm">Theme</span>
|
314 |
+
</button>
|
315 |
+
<button class="btn primary" id="printBtn" title="Export to PDF">
|
316 |
+
<i data-feather="download-cloud"></i>
|
317 |
+
Export
|
318 |
+
</button>
|
319 |
+
</div>
|
320 |
+
</div>
|
321 |
+
<div class="progressbar" id="progressbar"></div>
|
322 |
+
</header>
|
323 |
+
|
324 |
+
<main class="wrap">
|
325 |
+
<div class="layout">
|
326 |
+
<aside class="toc" id="toc">
|
327 |
+
<h4><i data-feather="list"></i> On this page</h4>
|
328 |
+
<nav id="tocLinks"></nav>
|
329 |
+
<div class="small" style="margin-top:.75rem;">Tip: Press / to search. Use ↑ ↓ to navigate results.</div>
|
330 |
+
</aside>
|
331 |
+
|
332 |
+
<div class="main">
|
333 |
+
<section class="hero">
|
334 |
+
<h1>Vietnam 2025: Momentum With Vigilance</h1>
|
335 |
+
<div class="subhead">GDP surged 7.96% in Q2 and 7.52% in H1—best first-half since 2011—powered by industry and services. Inflation is contained, unemployment remains low, and FDI inflows are robust amid global headwinds.</div>
|
336 |
+
<div class="kpi-grid" id="kpiGrid">
|
337 |
+
<div class="kpi">
|
338 |
+
<div class="bg"></div>
|
339 |
+
<div class="label">GDP Growth Q2 2025 (y/y)</div>
|
340 |
+
<div class="value" data-kpi="gdp_q2">7.96%</div>
|
341 |
+
<div class="delta"><i data-feather="trending-up"></i> Above Q1</div>
|
342 |
+
<div class="actions">
|
343 |
+
<button class="btn ghost copy-kpi" data-copy="GDP Q2 2025: 7.96%">Copy</button>
|
344 |
+
<button class="btn ghost link-kpi" data-link="#gdp-indicators">Jump</button>
|
345 |
+
</div>
|
346 |
+
</div>
|
347 |
+
<div class="kpi">
|
348 |
+
<div class="bg"></div>
|
349 |
+
<div class="label">GDP Growth H1 2025</div>
|
350 |
+
<div class="value" data-kpi="gdp_h1">7.52%</div>
|
351 |
+
<div class="delta"><i data-feather="award"></i> Highest since 2011</div>
|
352 |
+
<div class="actions">
|
353 |
+
<button class="btn ghost copy-kpi" data-copy="GDP H1 2025: 7.52%">Copy</button>
|
354 |
+
<button class="btn ghost link-kpi" data-link="#gdp-indicators">Jump</button>
|
355 |
+
</div>
|
356 |
+
</div>
|
357 |
+
<div class="kpi">
|
358 |
+
<div class="bg"></div>
|
359 |
+
<div class="label">Inflation (June 2025)</div>
|
360 |
+
<div class="value">3.57%</div>
|
361 |
+
<div class="delta" style="color:var(--warn); background: color-mix(in oklab, var(--warn) 14%, transparent);">
|
362 |
+
<i data-feather="chevron-up"></i> Highest YTD
|
363 |
+
</div>
|
364 |
+
<div class="actions">
|
365 |
+
<button class="btn ghost copy-kpi" data-copy="Inflation June 2025: 3.57%">Copy</button>
|
366 |
+
<button class="btn ghost link-kpi" data-link="#inflation">Jump</button>
|
367 |
+
</div>
|
368 |
+
</div>
|
369 |
+
<div class="kpi">
|
370 |
+
<div class="bg"></div>
|
371 |
+
<div class="label">Unemployment (Q1 2025)</div>
|
372 |
+
<div class="value">2.20%</div>
|
373 |
+
<div class="delta" style="color:var(--ok); background: color-mix(in oklab, var(--ok) 14%, transparent);">
|
374 |
+
<i data-feather="trending-down"></i> From Q4 2024
|
375 |
+
</div>
|
376 |
+
<div class="actions">
|
377 |
+
<button class="btn ghost copy-kpi" data-copy="Unemployment Q1 2025: 2.20%">Copy</button>
|
378 |
+
<button class="btn ghost link-kpi" data-link="#labor">Jump</button>
|
379 |
+
</div>
|
380 |
+
</div>
|
381 |
+
<div class="kpi">
|
382 |
+
<div class="bg"></div>
|
383 |
+
<div class="label">FDI (H1 2025)</div>
|
384 |
+
<div class="value">$21.51B</div>
|
385 |
+
<div class="delta" style="color:var(--accent-2); background: color-mix(in oklab, var(--accent-2) 14%, transparent);">
|
386 |
+
<i data-feather="plus-circle"></i> +32.6% y/y
|
387 |
+
</div>
|
388 |
+
<div class="actions">
|
389 |
+
<button class="btn ghost copy-kpi" data-copy="FDI H1 2025: $21.51B (+32.6% y/y)">Copy</button>
|
390 |
+
<button class="btn ghost link-kpi" data-link="#fdi">Jump</button>
|
391 |
+
</div>
|
392 |
+
</div>
|
393 |
+
<div class="kpi">
|
394 |
+
<div class="bg"></div>
|
395 |
+
<div class="label">Retail Sales (Q1 2025)</div>
|
396 |
+
<div class="value">₫1,708T</div>
|
397 |
+
<div class="delta"><i data-feather="shopping-bag"></i> +9.9% y/y</div>
|
398 |
+
<div class="actions">
|
399 |
+
<button class="btn ghost copy-kpi" data-copy="Retail Q1 2025: ₫1,708T (+9.9% y/y)">Copy</button>
|
400 |
+
<button class="btn ghost link-kpi" data-link="#sectoral">Jump</button>
|
401 |
+
</div>
|
402 |
+
</div>
|
403 |
+
</div>
|
404 |
+
</section>
|
405 |
+
|
406 |
+
<section class="section with-toolbar" id="executive-summary" data-title="Executive Summary">
|
407 |
+
<div class="toolbar-row">
|
408 |
+
<h2>Executive Summary</h2>
|
409 |
+
<div class="tools">
|
410 |
+
<button class="btn ghost copy-section" data-target="executive-summary"><i data-feather="clipboard"></i> Copy</button>
|
411 |
+
<button class="btn ghost link-section" data-target="executive-summary"><i data-feather="link-2"></i> Link</button>
|
412 |
+
</div>
|
413 |
+
</div>
|
414 |
+
<div class="summary">
|
415 |
+
Vietnam’s economy sustained robust momentum in 2025. GDP expanded 7.96% y/y in Q2 and 7.52% in H1 (best since 2011). Growth was anchored by services and manufacturing, while inflation remained within the 3–4.5% target band. Unemployment stayed historically low at 2.20%, and FDI inflows accelerated, signaling strong investor confidence despite external frictions from trade tensions and tariffs.
|
416 |
+
</div>
|
417 |
+
<ul>
|
418 |
+
<li>Growth leadership: services, manufacturing, and export industries; banking earnings seen up 17% with ~15% credit growth.</li>
|
419 |
+
<li>Risks: global trade tensions, tariff exposure, geopolitical uncertainty, and FDI overdependence concerns.</li>
|
420 |
+
<li>Policy stance: diversify markets, support domestic demand, preserve macro stability, and use fiscal space if needed.</li>
|
421 |
+
</ul>
|
422 |
+
</section>
|
423 |
+
|
424 |
+
<section class="section with-toolbar" id="gdp-indicators" data-title="Key Economic Indicators">
|
425 |
+
<div class="toolbar-row">
|
426 |
+
<h2>Key Economic Indicators 2025</h2>
|
427 |
+
<div class="tools">
|
428 |
+
<button class="btn ghost" id="downloadData"><i data-feather="download"></i> Data CSV</button>
|
429 |
+
<button class="btn ghost link-section" data-target="gdp-indicators"><i data-feather="link-2"></i> Link</button>
|
430 |
+
</div>
|
431 |
+
</div>
|
432 |
+
|
433 |
+
<div class="grid cols-2">
|
434 |
+
<div class="chart-card">
|
435 |
+
<h3>GDP Growth Performance</h3>
|
436 |
+
<div class="legend">
|
437 |
+
<span class="key"><span class="sw" style="background:var(--accent)"></span> Actual</span>
|
438 |
+
<span class="key"><span class="sw" style="background:var(--warn)"></span> Gov Target 8.3–8.5%</span>
|
439 |
+
</div>
|
440 |
+
<div class="chart" id="chart-gdp-bars" role="img" aria-label="Bar chart of GDP growth for Q1, Q2, and H1 2025"></div>
|
441 |
+
<div class="tools">
|
442 |
+
<button class="btn ghost export-chart" data-chart="chart-gdp-bars"><i data-feather="image"></i> Save PNG</button>
|
443 |
+
<div class="chip"><i data-feather="filter"></i>
|
444 |
+
<label style="display:inline-flex;gap:.35rem;align-items:center;">
|
445 |
+
<input type="checkbox" id="toggleTargetBand" checked /> Target band
|
446 |
+
</label>
|
447 |
+
</div>
|
448 |
+
</div>
|
449 |
+
</div>
|
450 |
+
|
451 |
+
<div class="chart-card">
|
452 |
+
<h3>2025 GDP Growth Forecasts</h3>
|
453 |
+
<div class="legend">
|
454 |
+
<span class="key"><span class="sw" style="background:var(--accent-3)"></span> International</span>
|
455 |
+
<span class="key"><span class="sw" style="background:var(--accent-2)"></span> Government target</span>
|
456 |
+
</div>
|
457 |
+
<div class="chart" id="chart-forecasts" role="img" aria-label="Dot plot of GDP forecasts"></div>
|
458 |
+
<div class="tools">
|
459 |
+
<div class="chip"><i data-feather="sliders"></i> Min forecast
|
460 |
+
<input type="range" id="forecastMin" min="0" max="9" step="0.1" value="0" style="width:160px;">
|
461 |
+
<span id="forecastMinVal" class="small">0%</span>
|
462 |
+
</div>
|
463 |
+
<button class="btn ghost export-chart" data-chart="chart-forecasts"><i data-feather="image"></i> Save PNG</button>
|
464 |
+
</div>
|
465 |
+
</div>
|
466 |
+
</div>
|
467 |
+
|
468 |
+
<details class="collapsible" style="margin-top:1rem;">
|
469 |
+
<summary>
|
470 |
+
<span>Indicator Table (Sortable)</span>
|
471 |
+
<i data-feather="chevron-down"></i>
|
472 |
+
</summary>
|
473 |
+
<div class="content">
|
474 |
+
<div class="table-wrap">
|
475 |
+
<table id="tblIndicators">
|
476 |
+
<thead>
|
477 |
+
<tr>
|
478 |
+
<th data-sort="text">Indicator</th>
|
479 |
+
<th data-sort="text">Period</th>
|
480 |
+
<th data-sort="num">Value</th>
|
481 |
+
<th data-sort="text">Notes</th>
|
482 |
+
</tr>
|
483 |
+
</thead>
|
484 |
+
<tbody>
|
485 |
+
<tr><td>GDP Growth</td><td>Q1 2025</td><td data-v="6.9">6.9%</td><td>y/y</td></tr>
|
486 |
+
<tr><td>GDP Growth</td><td>Q2 2025</td><td data-v="7.96">7.96%</td><td>y/y</td></tr>
|
487 |
+
<tr><td>GDP Growth</td><td>H1 2025</td><td data-v="7.52">7.52%</td><td>Highest since 2011</td></tr>
|
488 |
+
<tr><td>Inflation</td><td>May 2025</td><td data-v="3.24">3.24%</td><td>YTD</td></tr>
|
489 |
+
<tr><td>Inflation</td><td>June 2025</td><td data-v="3.57">3.57%</td><td>YTD high</td></tr>
|
490 |
+
<tr><td>Unemployment</td><td>Q1 2025</td><td data-v="2.2">2.20%</td><td>Down from 2.22%</td></tr>
|
491 |
+
<tr><td>FDI Registered</td><td>Jan–May 2025</td><td data-v="18.4">$18.4B</td><td>+51% y/y</td></tr>
|
492 |
+
<tr><td>FDI Disbursed</td><td>Jan–May 2025</td><td data-v="8.9">$8.9B</td><td> </td></tr>
|
493 |
+
<tr><td>FDI Total</td><td>H1 2025</td><td data-v="21.51">$21.51B</td><td>+32.6% y/y</td></tr>
|
494 |
+
</tbody>
|
495 |
+
</table>
|
496 |
+
</div>
|
497 |
+
</div>
|
498 |
+
</details>
|
499 |
+
</section>
|
500 |
+
|
501 |
+
<section class="section with-toolbar" id="inflation" data-title="Inflation Dynamics">
|
502 |
+
<div class="toolbar-row">
|
503 |
+
<h2>Inflation Rate</h2>
|
504 |
+
<div class="tools">
|
505 |
+
<span class="pill"><i data-feather="target"></i> Target: 3–4.5%</span>
|
506 |
+
<button class="btn ghost link-section" data-target="inflation"><i data-feather="link-2"></i> Link</button>
|
507 |
+
</div>
|
508 |
+
</div>
|
509 |
+
<p>Inflation remains well-controlled within the 3–4.5% target range. June’s 3.57% is the highest YTD but consistent with a manageable trajectory. External cost pressures from trade tensions are a watch point.</p>
|
510 |
+
<div class="grid cols-2">
|
511 |
+
<div class="chart-card">
|
512 |
+
<h3>Inflation Recent Prints & Forecasts</h3>
|
513 |
+
<div class="legend">
|
514 |
+
<span class="key"><span class="sw" style="background:var(--accent)"></span> Actual</span>
|
515 |
+
<span class="key"><span class="sw" style="background:var(--accent-3)"></span> IMF 2025: 2.9%</span>
|
516 |
+
<span class="key"><span class="sw" style="background:var(--warn)"></span> ADB 2025: 4.0%</span>
|
517 |
+
</div>
|
518 |
+
<div class="chart" id="chart-inflation"></div>
|
519 |
+
<div class="tools">
|
520 |
+
<button class="btn ghost export-chart" data-chart="chart-inflation"><i data-feather="image"></i> Save PNG</button>
|
521 |
+
</div>
|
522 |
+
</div>
|
523 |
+
|
524 |
+
<div class="chart-card">
|
525 |
+
<h3>Labor Market Snapshot</h3>
|
526 |
+
<div class="legend">
|
527 |
+
<span class="key"><span class="sw" style="background:var(--accent-2)"></span> Unemployment</span>
|
528 |
+
</div>
|
529 |
+
<div class="chart" id="chart-labor"></div>
|
530 |
+
<div class="tools">
|
531 |
+
<button class="btn ghost export-chart" data-chart="chart-labor"><i data-feather="image"></i> Save PNG</button>
|
532 |
+
</div>
|
533 |
+
</div>
|
534 |
+
</div>
|
535 |
+
</section>
|
536 |
+
|
537 |
+
<section class="section with-toolbar" id="fdi" data-title="Foreign Direct Investment">
|
538 |
+
<div class="toolbar-row">
|
539 |
+
<h2>Foreign Direct Investment</h2>
|
540 |
+
<div class="tools">
|
541 |
+
<button class="btn ghost link-section" data-target="fdi"><i data-feather="link-2"></i> Link</button>
|
542 |
+
</div>
|
543 |
+
</div>
|
544 |
+
<p>FDI inflows are robust, reflecting sustained foreign investor confidence. Registered capital surged while disbursements kept pace, and H1 totals surpassed $21B.</p>
|
545 |
+
<div class="grid cols-2">
|
546 |
+
<div class="chart-card">
|
547 |
+
<h3>FDI Momentum</h3>
|
548 |
+
<div class="legend">
|
549 |
+
<span class="key"><span class="sw" style="background:var(--accent)"></span> Registered (Jan–May)</span>
|
550 |
+
<span class="key"><span class="sw" style="background:var(--accent-3)"></span> Disbursed (Jan–May)</span>
|
551 |
+
<span class="key"><span class="sw" style="background:var(--accent-2)"></span> Total (H1)</span>
|
552 |
+
</div>
|
553 |
+
<div class="chart" id="chart-fdi"></div>
|
554 |
+
<div class="tools">
|
555 |
+
<button class="btn ghost export-chart" data-chart="chart-fdi"><i data-feather="image"></i> Save PNG</button>
|
556 |
+
</div>
|
557 |
+
</div>
|
558 |
+
<div class="section" style="margin:0;">
|
559 |
+
<h3>Investor Takeaways</h3>
|
560 |
+
<ul>
|
561 |
+
<li>Strength across diversified manufacturing and services clusters.</li>
|
562 |
+
<li>Policy continuity and infrastructure investments support pipeline.</li>
|
563 |
+
<li>Monitor sectoral concentration and supply-chain dependencies.</li>
|
564 |
+
</ul>
|
565 |
+
<div class="info">Policy note: Maintain macro buffers; prioritize quality FDI with technology transfer and value chain deepening.</div>
|
566 |
+
</div>
|
567 |
+
</div>
|
568 |
+
</section>
|
569 |
+
|
570 |
+
<section class="section with-toolbar" id="sectoral" data-title="Sectoral Analysis">
|
571 |
+
<div class="toolbar-row">
|
572 |
+
<h2>Sectoral Analysis</h2>
|
573 |
+
<div class="tools">
|
574 |
+
<button class="btn ghost link-section" data-target="sectoral"><i data-feather="link-2"></i> Link</button>
|
575 |
+
</div>
|
576 |
+
</div>
|
577 |
+
<div class="chips" id="sectorFilters">
|
578 |
+
<label class="chip"><input type="checkbox" data-sector="services" checked> Services</label>
|
579 |
+
<label class="chip"><input type="checkbox" data-sector="manufacturing" checked> Manufacturing</label>
|
580 |
+
<label class="chip"><input type="checkbox" data-sector="export" checked> Export Industries</label>
|
581 |
+
<label class="chip"><input type="checkbox" data-sector="banking" checked> Banking</label>
|
582 |
+
</div>
|
583 |
+
<div class="grid cols-2 cols-3-xl" id="sectorCards">
|
584 |
+
<div class="chart-card sector services">
|
585 |
+
<h3>Services</h3>
|
586 |
+
<p>Primary growth engine with broad-based expansion across retail, logistics, tourism, and digital services.</p>
|
587 |
+
</div>
|
588 |
+
<div class="chart-card sector manufacturing">
|
589 |
+
<h3>Manufacturing</h3>
|
590 |
+
<p>Recovery maintained with strong export orders; supply-chain realignment favors Vietnam’s cost-quality mix.</p>
|
591 |
+
</div>
|
592 |
+
<div class="chart-card sector export">
|
593 |
+
<h3>Export Industries</h3>
|
594 |
+
<p>Remain the economic backbone; competitiveness intact despite tariff-related frictions.</p>
|
595 |
+
</div>
|
596 |
+
<div class="chart-card sector banking">
|
597 |
+
<h3>Banking</h3>
|
598 |
+
<p>Earnings projected +17% in 2025 on ~15% system-wide credit growth; watch asset quality under external stress.</p>
|
599 |
+
</div>
|
600 |
+
</div>
|
601 |
+
</section>
|
602 |
+
|
603 |
+
<section class="section with-toolbar" id="risks" data-title="Challenges and Risks">
|
604 |
+
<div class="toolbar-row">
|
605 |
+
<h2>Challenges and Risk Factors</h2>
|
606 |
+
<div class="tools">
|
607 |
+
<button class="btn ghost link-section" data-target="risks"><i data-feather="link-2"></i> Link</button>
|
608 |
+
</div>
|
609 |
+
</div>
|
610 |
+
<ul>
|
611 |
+
<li>Global trade tensions and US tariff policies pressure export-oriented sectors.</li>
|
612 |
+
<li>Geopolitical instability raises uncertainty and potential cost pass-through.</li>
|
613 |
+
<li>Overreliance on FDI could amplify cyclicality and external shocks.</li>
|
614 |
+
<li>Imperative: growth not at the cost of macro stability, public debt, or inflation.</li>
|
615 |
+
</ul>
|
616 |
+
<div class="notice"><strong>Mitigation:</strong> diversify export markets, strengthen domestic demand, enhance resilience, and maintain fiscal space for counter-cyclical support.</div>
|
617 |
+
</section>
|
618 |
+
|
619 |
+
<section class="section with-toolbar" id="history" data-title="Historical Comparison">
|
620 |
+
<div class="toolbar-row">
|
621 |
+
<h2>Historical Comparison</h2>
|
622 |
+
<div class="tools">
|
623 |
+
<button class="btn ghost link-section" data-target="history"><i data-feather="link-2"></i> Link</button>
|
624 |
+
</div>
|
625 |
+
</div>
|
626 |
+
<p>Vietnam sustained high growth in 2024 (7.1%). Growth in 2025 may moderate versus ambitious targets due to external constraints, but fundamentals remain resilient.</p>
|
627 |
+
<div class="chart-card">
|
628 |
+
<h3>Q1 GDP Growth 2020–2025 (y/y)</h3>
|
629 |
+
<div class="legend">
|
630 |
+
<span class="key"><span class="sw" style="background:var(--accent)"></span> Q1 Growth</span>
|
631 |
+
<span class="key"><span class="sw" style="background:var(--accent-2)"></span> Trend</span>
|
632 |
+
</div>
|
633 |
+
<div class="chart" id="chart-history"></div>
|
634 |
+
<div class="tools">
|
635 |
+
<button class="btn ghost export-chart" data-chart="chart-history"><i data-feather="image"></i> Save PNG</button>
|
636 |
+
</div>
|
637 |
+
</div>
|
638 |
+
</section>
|
639 |
+
|
640 |
+
<section class="section with-toolbar" id="outlook" data-title="Economic Outlook">
|
641 |
+
<div class="toolbar-row">
|
642 |
+
<h2>Economic Outlook and Projections</h2>
|
643 |
+
<div class="tools">
|
644 |
+
<button class="btn ghost link-section" data-target="outlook"><i data-feather="link-2"></i> Link</button>
|
645 |
+
</div>
|
646 |
+
</div>
|
647 |
+
<div class="grid cols-2">
|
648 |
+
<div>
|
649 |
+
<h3>Near-term Prospects (2025)</h3>
|
650 |
+
<p>Solid starting point in Q1 (6.9% y/y) with resilience expected despite uncertainty. Government’s 8.3–8.5% target is ambitious vs international forecasts, but domestic drivers provide ballast.</p>
|
651 |
+
<h4>Key Supporting Factors</h4>
|
652 |
+
<ul>
|
653 |
+
<li>Robust FDI inflows and investor confidence</li>
|
654 |
+
<li>Low unemployment supporting consumption</li>
|
655 |
+
<li>Controlled inflation sustaining purchasing power</li>
|
656 |
+
<li>Export competitiveness and market diversification</li>
|
657 |
+
<li>Parliamentary push to raise GDP growth to at least 8%</li>
|
658 |
+
</ul>
|
659 |
+
</div>
|
660 |
+
<div>
|
661 |
+
<h3>Policy Playbook</h3>
|
662 |
+
<ul>
|
663 |
+
<li>Diversify export markets and deepen regional value chains.</li>
|
664 |
+
<li>Strengthen domestic demand through targeted measures.</li>
|
665 |
+
<li>Enhance resilience: buffers, liquidity backstops, prudent credit growth.</li>
|
666 |
+
<li>Use fiscal space counter-cyclically if global shocks intensify.</li>
|
667 |
+
</ul>
|
668 |
+
<div class="info">Bottom line: Cautiously optimistic baseline with upside from supply-chain shifts; vigilance on external risks is warranted.</div>
|
669 |
+
</div>
|
670 |
+
</div>
|
671 |
+
</section>
|
672 |
+
|
673 |
+
<section class="section with-toolbar" id="conclusion" data-title="Conclusion">
|
674 |
+
<div class="toolbar-row">
|
675 |
+
<h2>Conclusion</h2>
|
676 |
+
<div class="tools">
|
677 |
+
<button class="btn ghost link-section" data-target="conclusion"><i data-feather="link-2"></i> Link</button>
|
678 |
+
</div>
|
679 |
+
</div>
|
680 |
+
<p>Vietnam’s 2025 performance highlights resilience and strong fundamentals: low unemployment, contained inflation, and buoyant FDI. While international projections are more conservative than the government’s 8%+ target, momentum and reform commitment provide a solid backdrop for sustained development.</p>
|
681 |
+
</section>
|
682 |
+
|
683 |
+
<section class="section with-toolbar" id="methodology" data-title="Methodology">
|
684 |
+
<div class="toolbar-row">
|
685 |
+
<h2>Research Methodology</h2>
|
686 |
+
<div class="tools">
|
687 |
+
<button class="btn ghost link-section" data-target="methodology"><i data-feather="link-2"></i> Link</button>
|
688 |
+
</div>
|
689 |
+
</div>
|
690 |
+
<details class="collapsible">
|
691 |
+
<summary>Scope, Sources, and Processing <i data-feather="chevron-down"></i></summary>
|
692 |
+
<div class="content">
|
693 |
+
<ul>
|
694 |
+
<li>Scope: Macroeconomic indicators for Vietnam 2024–2025 with emphasis on Q1/Q2 2025 and H1 aggregates.</li>
|
695 |
+
<li>Sources: IMF, ADB, World Bank, GSO Vietnam, Trading Economics, Vietnam Investment Review, and related portals.</li>
|
696 |
+
<li>Processing: Extracted key values, normalized units, and constructed trend series for visualization. Forecasts aggregated for cross-comparison.</li>
|
697 |
+
<li>Validation: Cross-checked ranges and narratives against cited sources; flagged assumptions where applicable.</li>
|
698 |
+
</ul>
|
699 |
+
</div>
|
700 |
+
</details>
|
701 |
+
</section>
|
702 |
+
|
703 |
+
<section class="section with-toolbar" id="references" data-title="Sources & Citations">
|
704 |
+
<div class="toolbar-row">
|
705 |
+
<h2>Sources and Citations</h2>
|
706 |
+
<div class="tools">
|
707 |
+
<button class="btn ghost link-section" data-target="references"><i data-feather="link-2"></i> Link</button>
|
708 |
+
</div>
|
709 |
+
</div>
|
710 |
+
<div id="citationList" class="grid">
|
711 |
+
<div class="table-wrap">
|
712 |
+
<table>
|
713 |
+
<thead>
|
714 |
+
<tr><th>#</th><th>Source</th><th>URL</th><th>Actions</th></tr>
|
715 |
+
</thead>
|
716 |
+
<tbody>
|
717 |
+
<tr><td>1</td><td>Trading Economics - Vietnam GDP Annual Growth Rate</td><td><a href="https://tradingeconomics.com/vietnam/gdp-growth-annual" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Trading Economics - Vietnam GDP Annual Growth Rate: https://tradingeconomics.com/vietnam/gdp-growth-annual"><i data-feather="copy"></i> Copy</button></td></tr>
|
718 |
+
<tr><td>2</td><td>International Monetary Fund - Vietnam Country Profile</td><td><a href="https://www.imf.org/en/Countries/VNM" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="IMF - Vietnam Country Profile: https://www.imf.org/en/Countries/VNM"><i data-feather="copy"></i> Copy</button></td></tr>
|
719 |
+
<tr><td>3</td><td>World Economics - Vietnam GDP Estimates</td><td><a href="https://www.worldeconomics.com/GDP/Vietnam.gdp" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="World Economics - Vietnam GDP Estimates: https://www.worldeconomics.com/GDP/Vietnam.gdp"><i data-feather="copy"></i> Copy</button></td></tr>
|
720 |
+
<tr><td>4</td><td>General Statistics Office (Vietnam)</td><td><a href="https://www.gso.gov.vn/en/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="General Statistics Office (Vietnam): https://www.gso.gov.vn/en/"><i data-feather="copy"></i> Copy</button></td></tr>
|
721 |
+
<tr><td>5</td><td>Wikipedia - Economy of Vietnam</td><td><a href="https://en.wikipedia.org/wiki/Economy_of_Vietnam" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Wikipedia - Economy of Vietnam: https://en.wikipedia.org/wiki/Economy_of_Vietnam"><i data-feather="copy"></i> Copy</button></td></tr>
|
722 |
+
<tr><td>6</td><td>IMF - Vietnam and the IMF</td><td><a href="https://www.imf.org/en/Countries/VNM" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="IMF - Vietnam and the IMF: https://www.imf.org/en/Countries/VNM"><i data-feather="copy"></i> Copy</button></td></tr>
|
723 |
+
<tr><td>7</td><td>FocusEconomics - Vietnam Economic Indicators</td><td><a href="https://www.focus-economics.com/countries/vietnam" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="FocusEconomics - Vietnam Economic Indicators: https://www.focus-economics.com/countries/vietnam"><i data-feather="copy"></i> Copy</button></td></tr>
|
724 |
+
<tr><td>8</td><td>GSO Vietnam - Data & Statistics</td><td><a href="https://www.gso.gov.vn/en/data-and-statistics/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="GSO Vietnam - Data & Statistics: https://www.gso.gov.vn/en/data-and-statistics/"><i data-feather="copy"></i> Copy</button></td></tr>
|
725 |
+
<tr><td>9</td><td>VietnamNet - Economic News</td><td><a href="https://vietnamnet.vn/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="VietnamNet - Economic News: https://vietnamnet.vn/"><i data-feather="copy"></i> Copy</button></td></tr>
|
726 |
+
<tr><td>10</td><td>IMF - Article IV Mission Reports</td><td><a href="https://www.imf.org/en/Publications/CR" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="IMF - Article IV Mission Reports: https://www.imf.org/en/Publications/CR"><i data-feather="copy"></i> Copy</button></td></tr>
|
727 |
+
<tr><td>11</td><td>Vietnam Briefing - Analysis</td><td><a href="https://www.vietnam-briefing.com/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Vietnam Briefing - Economic Analysis: https://www.vietnam-briefing.com/"><i data-feather="copy"></i> Copy</button></td></tr>
|
728 |
+
<tr><td>12</td><td>Vietnam Investment Review - FDI</td><td><a href="https://vir.com.vn/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Vietnam Investment Review - FDI Statistics: https://vir.com.vn/"><i data-feather="copy"></i> Copy</button></td></tr>
|
729 |
+
<tr><td>13</td><td>Trading Economics - FDI</td><td><a href="https://tradingeconomics.com/vietnam/foreign-direct-investment" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Trading Economics - Vietnam FDI: https://tradingeconomics.com/vietnam/foreign-direct-investment"><i data-feather="copy"></i> Copy</button></td></tr>
|
730 |
+
<tr><td>14</td><td>White & Case - Regional Outlook</td><td><a href="https://www.whitecase.com/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="White & Case - Regional Economic Outlook: https://www.whitecase.com/"><i data-feather="copy"></i> Copy</button></td></tr>
|
731 |
+
<tr><td>15</td><td>Vietnam Economic Times</td><td><a href="https://vneconomictimes.com/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Vietnam Economic Times: https://vneconomictimes.com/"><i data-feather="copy"></i> Copy</button></td></tr>
|
732 |
+
<tr><td>16</td><td>Asian Development Bank - Viet Nam</td><td><a href="https://www.adb.org/countries/viet-nam/main" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="ADB - Viet Nam Country Partnership: https://www.adb.org/countries/viet-nam/main"><i data-feather="copy"></i> Copy</button></td></tr>
|
733 |
+
<tr><td>17</td><td>Ministry of Planning and Investment</td><td><a href="https://www.mpi.gov.vn/en/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Ministry of Planning and Investment (Vietnam): https://www.mpi.gov.vn/en/"><i data-feather="copy"></i> Copy</button></td></tr>
|
734 |
+
</tbody>
|
735 |
+
</table>
|
736 |
+
</div>
|
737 |
+
</div>
|
738 |
+
</section>
|
739 |
+
|
740 |
+
<section class="section with-toolbar" id="appendix" data-title="Appendices">
|
741 |
+
<div class="toolbar-row">
|
742 |
+
<h2>Appendices</h2>
|
743 |
+
<div class="tools">
|
744 |
+
<button class="btn ghost link-section" data-target="appendix"><i data-feather="link-2"></i> Link</button>
|
745 |
+
</div>
|
746 |
+
</div>
|
747 |
+
<details class="collapsible">
|
748 |
+
<summary>Data Dictionary <i data-feather="chevron-down"></i></summary>
|
749 |
+
<div class="content">
|
750 |
+
<ul>
|
751 |
+
<li>GDP Growth: Real GDP y/y change, quarterly and half-year aggregate.</li>
|
752 |
+
<li>Inflation: Headline CPI y/y.</li>
|
753 |
+
<li>Unemployment: National unemployment rate.</li>
|
754 |
+
<li>FDI: Registered and disbursed capital in USD; total H1 in USD.</li>
|
755 |
+
</ul>
|
756 |
+
</div>
|
757 |
+
</details>
|
758 |
+
<details class="collapsible" style="margin-top:.75rem;">
|
759 |
+
<summary>Assumptions & Notes <i data-feather="chevron-down"></i></summary>
|
760 |
+
<div class="content">
|
761 |
+
<ul>
|
762 |
+
<li>Q1 historical series reflects reported year-on-year expansions 2020–2025.</li>
|
763 |
+
<li>Forecast comparison normalizes to calendar 2025 growth.</li>
|
764 |
+
</ul>
|
765 |
+
</div>
|
766 |
+
</details>
|
767 |
+
</section>
|
768 |
+
|
769 |
+
<footer class="foot">
|
770 |
+
<div class="meta-row">
|
771 |
+
<span class="pill"><i data-feather="shield"></i> Stable macro</span>
|
772 |
+
<span class="pill"><i data-feather="cpu"></i> Manufacturing hub</span>
|
773 |
+
<span class="pill"><i data-feather="trending-up"></i> FDI momentum</span>
|
774 |
+
</div>
|
775 |
+
<div class="small">© 2025 Research Dashboard. Built with vanilla Web APIs. Use the Export button to print to PDF or save charts as PNG.</div>
|
776 |
+
</footer>
|
777 |
+
|
778 |
+
<div class="sticky-tools">
|
779 |
+
<div class="floater">
|
780 |
+
<button class="btn" id="backToTop"><i data-feather="arrow-up"></i> Top</button>
|
781 |
+
<button class="btn" id="nextSection"><i data-feather="chevron-down"></i> Next</button>
|
782 |
+
</div>
|
783 |
+
</div>
|
784 |
+
</div>
|
785 |
+
</div>
|
786 |
+
</main>
|
787 |
+
|
788 |
+
<script>
|
789 |
+
feather.replace();
|
790 |
+
|
791 |
+
// State & Data
|
792 |
+
const state = {
|
793 |
+
theme: localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'),
|
794 |
+
search: { index: [], matches: [], current: -1 },
|
795 |
+
charts: {}
|
796 |
+
};
|
797 |
+
document.documentElement.setAttribute('data-theme', state.theme);
|
798 |
+
|
799 |
+
// Datasets
|
800 |
+
const data = {
|
801 |
+
gdpBars: [
|
802 |
+
{label: 'Q1 2025', value: 6.9},
|
803 |
+
{label: 'Q2 2025', value: 7.96},
|
804 |
+
{label: 'H1 2025', value: 7.52}
|
805 |
+
],
|
806 |
+
forecast: [
|
807 |
+
{name: 'World Bank', value: 5.8, type: 'intl'},
|
808 |
+
{name: 'ADB', value: 6.6, type: 'intl'},
|
809 |
+
{name: 'IMF', value: 5.2, type: 'intl'},
|
810 |
+
{name: 'Gov Target Lower', value: 8.3, type: 'gov'},
|
811 |
+
{name: 'Gov Target Upper', value: 8.5, type: 'gov'}
|
812 |
+
],
|
813 |
+
inflation: [
|
814 |
+
{month: 'May 2025', value: 3.24},
|
815 |
+
{month: 'June 2025', value: 3.57}
|
816 |
+
],
|
817 |
+
inflationForecasts: [
|
818 |
+
{name: 'IMF 2025', value: 2.9, color: 'var(--accent-3)'},
|
819 |
+
{name: 'ADB 2025', value: 4.0, color: 'var(--warn)'}
|
820 |
+
],
|
821 |
+
unemployment: [
|
822 |
+
{label: 'Q4 2024', value: 2.22},
|
823 |
+
{label: 'Q1 2025', value: 2.20}
|
824 |
+
],
|
825 |
+
fdi: [
|
826 |
+
{label: 'Registered (Jan–May)', value: 18.4, color: 'var(--accent)'},
|
827 |
+
{label: 'Disbursed (Jan–May)', value: 8.9, color: 'var(--accent-3)'},
|
828 |
+
{label: 'Total (H1)', value: 21.51, color: 'var(--accent-2)'}
|
829 |
+
],
|
830 |
+
historyQ1: [
|
831 |
+
{year: 2020, value: 3.21},
|
832 |
+
{year: 2021, value: 4.85},
|
833 |
+
{year: 2022, value: 5.42},
|
834 |
+
{year: 2023, value: 3.46},
|
835 |
+
{year: 2024, value: 5.98},
|
836 |
+
{year: 2025, value: 6.93}
|
837 |
+
]
|
838 |
+
};
|
839 |
+
|
840 |
+
// Utility: formatters
|
841 |
+
const fmt = {
|
842 |
+
pct: v => `${(Math.round(v * 100) / 100).toFixed(2)}%`,
|
843 |
+
moneyB: v => `$${v.toFixed(2)}B`
|
844 |
+
};
|
845 |
+
|
846 |
+
// Theme toggle
|
847 |
+
document.getElementById('themeToggle').addEventListener('click', () => {
|
848 |
+
state.theme = (state.theme === 'light') ? 'dark' : 'light';
|
849 |
+
document.documentElement.setAttribute('data-theme', state.theme);
|
850 |
+
localStorage.setItem('theme', state.theme);
|
851 |
+
});
|
852 |
+
|
853 |
+
// Print/Export
|
854 |
+
document.getElementById('printBtn').addEventListener('click', () => {
|
855 |
+
window.print();
|
856 |
+
});
|
857 |
+
|
858 |
+
// Build TOC
|
859 |
+
const sections = Array.from(document.querySelectorAll('.section, .hero')).map(el => {
|
860 |
+
if (!el.id) el.id = 'section-' + Math.random().toString(36).slice(2,7);
|
861 |
+
return el;
|
862 |
+
});
|
863 |
+
const tocLinks = document.getElementById('tocLinks');
|
864 |
+
const tocFrag = document.createDocumentFragment();
|
865 |
+
sections.forEach(sec => {
|
866 |
+
const title = sec.dataset.title || sec.querySelector('h1,h2')?.textContent?.trim() || 'Section';
|
867 |
+
const a = document.createElement('a');
|
868 |
+
a.href = `#${sec.id}`;
|
869 |
+
a.textContent = title;
|
870 |
+
tocFrag.appendChild(a);
|
871 |
+
});
|
872 |
+
tocLinks.appendChild(tocFrag);
|
873 |
+
|
874 |
+
// IntersectionObserver: active section & progress
|
875 |
+
const progress = document.getElementById('progressbar');
|
876 |
+
const linkMap = new Map(Array.from(tocLinks.querySelectorAll('a')).map(a => [a.getAttribute('href').slice(1), a]));
|
877 |
+
const io = new IntersectionObserver(entries => {
|
878 |
+
entries.forEach(entry => {
|
879 |
+
if (entry.isIntersecting) {
|
880 |
+
linkMap.forEach(a => a.classList.remove('active'));
|
881 |
+
const a = linkMap.get(entry.target.id);
|
882 |
+
if (a) a.classList.add('active');
|
883 |
+
}
|
884 |
+
});
|
885 |
+
const viewport = window.scrollY + window.innerHeight;
|
886 |
+
const doc = document.body.scrollHeight;
|
887 |
+
const pct = Math.min(100, Math.max(0, (viewport / doc) * 100));
|
888 |
+
progress.style.width = pct + '%';
|
889 |
+
}, {rootMargin: '-40% 0px -55% 0px', threshold: [0, 0.25, 0.5, 1]});
|
890 |
+
sections.forEach(sec => io.observe(sec));
|
891 |
+
|
892 |
+
// Smooth link buttons
|
893 |
+
document.querySelectorAll('.link-section').forEach(btn => btn.addEventListener('click', e => {
|
894 |
+
const id = e.currentTarget.dataset.target;
|
895 |
+
location.hash = id;
|
896 |
+
document.getElementById(id)?.scrollIntoView({behavior: 'smooth', block: 'start'});
|
897 |
+
}));
|
898 |
+
document.querySelectorAll('.link-kpi').forEach(btn => btn.addEventListener('click', e => {
|
899 |
+
const id = e.currentTarget.dataset.link.slice(1);
|
900 |
+
document.getElementById(id)?.scrollIntoView({behavior: 'smooth', block: 'start'});
|
901 |
+
}));
|
902 |
+
|
903 |
+
// Copy KPI buttons
|
904 |
+
const copyText = async (txt) => {
|
905 |
+
try {
|
906 |
+
await navigator.clipboard.writeText(txt);
|
907 |
+
toast('Copied to clipboard');
|
908 |
+
} catch {
|
909 |
+
const ta = document.createElement('textarea'); ta.value = txt; document.body.appendChild(ta);
|
910 |
+
ta.select(); document.execCommand('copy'); ta.remove(); toast('Copied');
|
911 |
+
}
|
912 |
+
};
|
913 |
+
document.querySelectorAll('.copy-kpi').forEach(btn => btn.addEventListener('click', e => {
|
914 |
+
copyText(e.currentTarget.dataset.copy);
|
915 |
+
}));
|
916 |
+
document.querySelectorAll('.copy-section').forEach(btn => btn.addEventListener('click', e => {
|
917 |
+
const id = e.currentTarget.dataset.target;
|
918 |
+
const el = document.getElementById(id);
|
919 |
+
copyText(el.innerText.trim());
|
920 |
+
}));
|
921 |
+
document.querySelectorAll('.copy-cite').forEach(btn => btn.addEventListener('click', e => {
|
922 |
+
copyText(e.currentTarget.dataset.cite);
|
923 |
+
}));
|
924 |
+
|
925 |
+
// Back to top & Next section
|
926 |
+
document.getElementById('backToTop').addEventListener('click', () => window.scrollTo({top:0, behavior:'smooth'}));
|
927 |
+
document.getElementById('nextSection').addEventListener('click', () => {
|
928 |
+
const y = window.scrollY;
|
929 |
+
const next = sections.find(sec => sec.getBoundingClientRect().top + window.scrollY > y + 20);
|
930 |
+
next?.scrollIntoView({behavior: 'smooth', block: 'start'});
|
931 |
+
});
|
932 |
+
|
933 |
+
// Search with highlight and navigation
|
934 |
+
const searchInput = document.getElementById('globalSearch');
|
935 |
+
const clearSearch = () => {
|
936 |
+
document.querySelectorAll('.highlight').forEach(n => {
|
937 |
+
const parent = n.parentNode; parent.replaceChild(document.createTextNode(n.textContent), n); parent.normalize();
|
938 |
+
});
|
939 |
+
state.search.matches = []; state.search.current = -1;
|
940 |
+
};
|
941 |
+
document.getElementById('clearSearch').addEventListener('click', () => {
|
942 |
+
searchInput.value = ''; clearSearch();
|
943 |
+
});
|
944 |
+
const highlightAll = (term) => {
|
945 |
+
clearSearch();
|
946 |
+
if (!term) return;
|
947 |
+
const walker = document.createTreeWalker(document.querySelector('main'), NodeFilter.SHOW_TEXT, {
|
948 |
+
acceptNode: node => {
|
949 |
+
const t = node.nodeValue.trim();
|
950 |
+
if (!t || node.parentElement.closest('script,style,svg,code,pre,button,nav,header,footer')) return NodeFilter.FILTER_REJECT;
|
951 |
+
return t.toLowerCase().includes(term.toLowerCase()) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
952 |
+
}
|
953 |
+
});
|
954 |
+
const nodes = [];
|
955 |
+
while (walker.nextNode()) nodes.push(walker.currentNode);
|
956 |
+
nodes.forEach(textNode => {
|
957 |
+
const val = textNode.nodeValue;
|
958 |
+
const idx = val.toLowerCase().indexOf(term.toLowerCase());
|
959 |
+
if (idx > -1) {
|
960 |
+
const span = document.createElement('span');
|
961 |
+
span.className = 'highlight';
|
962 |
+
span.textContent = val.substr(idx, term.length);
|
963 |
+
const before = document.createTextNode(val.substr(0, idx));
|
964 |
+
const after = document.createTextNode(val.substr(idx + term.length));
|
965 |
+
const parent = textNode.parentNode;
|
966 |
+
parent.replaceChild(after, textNode);
|
967 |
+
parent.insertBefore(span, after);
|
968 |
+
parent.insertBefore(before, span);
|
969 |
+
}
|
970 |
+
});
|
971 |
+
state.search.matches = Array.from(document.querySelectorAll('.highlight'));
|
972 |
+
state.search.current = -1;
|
973 |
+
if (state.search.matches.length) gotoMatch(0);
|
974 |
+
};
|
975 |
+
const gotoMatch = (i) => {
|
976 |
+
if (!state.search.matches.length) return;
|
977 |
+
state.search.current = (i + state.search.matches.length) % state.search.matches.length;
|
978 |
+
const el = state.search.matches[state.search.current];
|
979 |
+
el.scrollIntoView({behavior:'smooth', block:'center'});
|
980 |
+
el.animate([{background: 'var(--mark)'}, {background: 'transparent'}], {duration: 1500, fill: 'forwards'});
|
981 |
+
};
|
982 |
+
searchInput.addEventListener('input', e => highlightAll(e.target.value.trim()));
|
983 |
+
window.addEventListener('keydown', (e) => {
|
984 |
+
if (e.key === '/' && document.activeElement !== searchInput) {
|
985 |
+
e.preventDefault(); searchInput.focus(); searchInput.select();
|
986 |
+
} else if ((e.key === 'Enter' || e.key === 'ArrowDown') && state.search.matches.length) {
|
987 |
+
gotoMatch(state.search.current + 1);
|
988 |
+
} else if (e.key === 'ArrowUp' && state.search.matches.length) {
|
989 |
+
gotoMatch(state.search.current - 1);
|
990 |
+
}
|
991 |
+
});
|
992 |
+
|
993 |
+
// Sortable tables
|
994 |
+
const sortTable = (table, colIndex, type, asc) => {
|
995 |
+
const tbody = table.tBodies[0];
|
996 |
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
997 |
+
rows.sort((a,b) => {
|
998 |
+
const av = a.children[colIndex].dataset.v ?? a.children[colIndex].textContent;
|
999 |
+
const bv = b.children[colIndex].dataset.v ?? b.children[colIndex].textContent;
|
1000 |
+
if (type === 'num') return (parseFloat(av) - parseFloat(bv)) * (asc ? 1 : -1);
|
1001 |
+
return av.toString().localeCompare(bv.toString()) * (asc ? 1 : -1);
|
1002 |
+
});
|
1003 |
+
rows.forEach(r => tbody.appendChild(r));
|
1004 |
+
};
|
1005 |
+
document.querySelectorAll('table thead th').forEach((th, i) => {
|
1006 |
+
let asc = true;
|
1007 |
+
th.addEventListener('click', () => {
|
1008 |
+
sortTable(th.closest('table'), i, th.dataset.sort, asc);
|
1009 |
+
asc = !asc;
|
1010 |
+
});
|
1011 |
+
});
|
1012 |
+
|
1013 |
+
// Export data CSV
|
1014 |
+
document.getElementById('downloadData').addEventListener('click', () => {
|
1015 |
+
const rows = [
|
1016 |
+
['Indicator','Period','Value','Notes'],
|
1017 |
+
['GDP Growth','Q1 2025','6.9%','y/y'],
|
1018 |
+
['GDP Growth','Q2 2025','7.96%','y/y'],
|
1019 |
+
['GDP Growth','H1 2025','7.52%','Highest since 2011'],
|
1020 |
+
['Inflation','May 2025','3.24%','YTD'],
|
1021 |
+
['Inflation','June 2025','3.57%','YTD high'],
|
1022 |
+
['Unemployment','Q1 2025','2.20%','Down from 2.22%'],
|
1023 |
+
['FDI Registered','Jan–May 2025','$18.4B','+51% y/y'],
|
1024 |
+
['FDI Disbursed','Jan–May 2025','$8.9B',''],
|
1025 |
+
['FDI Total','H1 2025','$21.51B','+32.6% y/y']
|
1026 |
+
];
|
1027 |
+
const csv = rows.map(r => r.map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n');
|
1028 |
+
const blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'});
|
1029 |
+
const url = URL.createObjectURL(blob);
|
1030 |
+
const a = document.createElement('a'); a.href = url; a.download = 'vietnam_2025_indicators.csv';
|
1031 |
+
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
|
1032 |
+
});
|
1033 |
+
|
1034 |
+
// Sector filters
|
1035 |
+
const sectorFilters = document.getElementById('sectorFilters');
|
1036 |
+
sectorFilters.addEventListener('change', () => {
|
1037 |
+
const active = new Set(Array.from(sectorFilters.querySelectorAll('input:checked')).map(i => i.dataset.sector));
|
1038 |
+
document.querySelectorAll('#sectorCards .sector').forEach(card => {
|
1039 |
+
const cat = [...card.classList].find(c => ['services','manufacturing','export','banking'].includes(c));
|
1040 |
+
card.style.display = active.has(cat) ? '' : 'none';
|
1041 |
+
});
|
1042 |
+
localStorage.setItem('sectorFilters', JSON.stringify([...active]));
|
1043 |
+
});
|
1044 |
+
// Restore sector filters
|
1045 |
+
const savedFilters = JSON.parse(localStorage.getItem('sectorFilters') || 'null');
|
1046 |
+
if (savedFilters) {
|
1047 |
+
sectorFilters.querySelectorAll('input').forEach(i => i.checked = savedFilters.includes(i.dataset.sector));
|
1048 |
+
sectorFilters.dispatchEvent(new Event('change'));
|
1049 |
+
}
|
1050 |
+
|
1051 |
+
// Small toasts
|
1052 |
+
let toastTimer;
|
1053 |
+
function toast(msg) {
|
1054 |
+
clearTimeout(toastTimer);
|
1055 |
+
let el = document.getElementById('toast');
|
1056 |
+
if (!el) {
|
1057 |
+
el = document.createElement('div');
|
1058 |
+
el.id = 'toast';
|
1059 |
+
el.style.position = 'fixed';
|
1060 |
+
el.style.bottom = '18px';
|
1061 |
+
el.style.left = '50%';
|
1062 |
+
el.style.transform = 'translateX(-50%)';
|
1063 |
+
el.style.background = 'var(--panel)';
|
1064 |
+
el.style.border = '1px solid var(--bdr)';
|
1065 |
+
el.style.boxShadow = 'var(--shadow)';
|
1066 |
+
el.style.color = 'var(--text)';
|
1067 |
+
el.style.padding = '.6rem .9rem';
|
1068 |
+
el.style.borderRadius = '10px';
|
1069 |
+
el.style.zIndex = '999';
|
1070 |
+
document.body.appendChild(el);
|
1071 |
+
}
|
1072 |
+
el.textContent = msg;
|
1073 |
+
el.style.opacity = '1';
|
1074 |
+
toastTimer = setTimeout(() => { el.style.transition = 'opacity .4s'; el.style.opacity = '0'; }, 1500);
|
1075 |
+
}
|
1076 |
+
|
1077 |
+
// Charts: SVG helpers
|
1078 |
+
function createSVG(w, h) {
|
1079 |
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
1080 |
+
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
|
1081 |
+
svg.setAttribute('width', '100%'); svg.setAttribute('height', '100%');
|
1082 |
+
return svg;
|
1083 |
+
}
|
1084 |
+
function axis(svg, x, y, w, h, ticks=5, max=10, format=v=>v.toString()) {
|
1085 |
+
const g = document.createElementNS(svg.namespaceURI, 'g');
|
1086 |
+
g.setAttribute('stroke', 'currentColor');
|
1087 |
+
g.setAttribute('opacity', '.5');
|
1088 |
+
for (let i=0;i<=ticks;i++) {
|
1089 |
+
const ty = y + h - (i/ticks)*h;
|
1090 |
+
const line = document.createElementNS(svg.namespaceURI, 'line');
|
1091 |
+
line.setAttribute('x1', x); line.setAttribute('x2', x+w);
|
1092 |
+
line.setAttribute('y1', ty); line.setAttribute('y2', ty);
|
1093 |
+
line.setAttribute('stroke-width', i===0?1.2:0.6);
|
1094 |
+
g.appendChild(line);
|
1095 |
+
const label = document.createElementNS(svg.namespaceURI, 'text');
|
1096 |
+
label.setAttribute('x', x - 6);
|
1097 |
+
label.setAttribute('y', ty + 4);
|
1098 |
+
label.setAttribute('font-size', '12');
|
1099 |
+
label.setAttribute('text-anchor', 'end');
|
1100 |
+
label.textContent = format((i/ticks)*max);
|
1101 |
+
g.appendChild(label);
|
1102 |
+
}
|
1103 |
+
svg.appendChild(g);
|
1104 |
+
}
|
1105 |
+
function exportSVGToPNG(svgEl, filename='chart.png') {
|
1106 |
+
const svgData = new XMLSerializer().serializeToString(svgEl);
|
1107 |
+
const canvas = document.createElement('canvas');
|
1108 |
+
const bbox = svgEl.viewBox.baseVal;
|
1109 |
+
canvas.width = bbox.width * 2;
|
1110 |
+
canvas.height = bbox.height * 2;
|
1111 |
+
const ctx = canvas.getContext('2d');
|
1112 |
+
const img = new Image();
|
1113 |
+
const svgBlob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
|
1114 |
+
const url = URL.createObjectURL(svgBlob);
|
1115 |
+
img.onload = function() {
|
1116 |
+
ctx.scale(2,2);
|
1117 |
+
ctx.drawImage(img, 0, 0);
|
1118 |
+
URL.revokeObjectURL(url);
|
1119 |
+
canvas.toBlob(function(blob) {
|
1120 |
+
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
|
1121 |
+
a.download = filename; document.body.appendChild(a); a.click(); a.remove();
|
1122 |
+
});
|
1123 |
+
};
|
1124 |
+
img.src = url;
|
1125 |
+
}
|
1126 |
+
|
1127 |
+
// Chart: Bars with optional target band
|
1128 |
+
function drawBars(containerId, series, opts = {}) {
|
1129 |
+
const el = document.getElementById(containerId);
|
1130 |
+
el.innerHTML = '';
|
1131 |
+
const svg = createSVG(800, 480);
|
1132 |
+
el.appendChild(svg);
|
1133 |
+
const pad = {l: 60, r: 20, t: 30, b: 60};
|
1134 |
+
const w = 800 - pad.l - pad.r;
|
1135 |
+
const h = 480 - pad.t - pad.b;
|
1136 |
+
const max = Math.max(opts.max || 10, ...series.map(d => d.value)) + 0.5;
|
1137 |
+
|
1138 |
+
// Target band
|
1139 |
+
if (opts.targetBand?.length && document.getElementById('toggleTargetBand')?.checked) {
|
1140 |
+
const [lo, hi] = opts.targetBand;
|
1141 |
+
const yLo = pad.t + h - (lo / max) * h;
|
1142 |
+
const yHi = pad.t + h - (hi / max) * h;
|
1143 |
+
const rect = document.createElementNS(svg.namespaceURI, 'rect');
|
1144 |
+
rect.setAttribute('x', pad.l);
|
1145 |
+
rect.setAttribute('width', w);
|
1146 |
+
rect.setAttribute('y', yHi);
|
1147 |
+
rect.setAttribute('height', Math.max(2, yLo - yHi));
|
1148 |
+
rect.setAttribute('fill', 'url(#bandGrad)');
|
1149 |
+
rect.setAttribute('opacity', '0.35');
|
1150 |
+
// gradient
|
1151 |
+
const defs = document.createElementNS(svg.namespaceURI, 'defs');
|
1152 |
+
const grad = document.createElementNS(svg.namespaceURI, 'linearGradient');
|
1153 |
+
grad.setAttribute('id','bandGrad'); grad.setAttribute('x1','0%'); grad.setAttribute('x2','0%'); grad.setAttribute('y1','0%'); grad.setAttribute('y2','100%');
|
1154 |
+
const s1 = document.createElementNS(svg.namespaceURI, 'stop'); s1.setAttribute('offset','0%'); s1.setAttribute('stop-color','var(--warn)');
|
1155 |
+
const s2 = document.createElementNS(svg.namespaceURI, 'stop'); s2.setAttribute('offset','100%'); s2.setAttribute('stop-color','var(--accent-2)');
|
1156 |
+
grad.appendChild(s1); grad.appendChild(s2); defs.appendChild(grad); svg.appendChild(defs);
|
1157 |
+
svg.appendChild(rect);
|
1158 |
+
}
|
1159 |
+
|
1160 |
+
axis(svg, pad.l, pad.t, w, h, 5, max, v => (v).toFixed(0) + '%');
|
1161 |
+
|
1162 |
+
const bw = w / (series.length * 1.5);
|
1163 |
+
series.forEach((d, i) => {
|
1164 |
+
const x = pad.l + i * (w / series.length) + (w / series.length - bw) / 2;
|
1165 |
+
const y = pad.t + h - (d.value / max) * h;
|
1166 |
+
const rect = document.createElementNS(svg.namespaceURI, 'rect');
|
1167 |
+
rect.setAttribute('x', x);
|
1168 |
+
rect.setAttribute('y', y);
|
1169 |
+
rect.setAttribute('width', bw);
|
1170 |
+
rect.setAttribute('height', Math.max(2, pad.t + h - y));
|
1171 |
+
rect.setAttribute('rx', '6');
|
1172 |
+
rect.setAttribute('fill', 'var(--accent)');
|
1173 |
+
rect.setAttribute('opacity', '.9');
|
1174 |
+
svg.appendChild(rect);
|
1175 |
+
|
1176 |
+
const lbl = document.createElementNS(svg.namespaceURI, 'text');
|
1177 |
+
lbl.setAttribute('x', x + bw/2);
|
1178 |
+
lbl.setAttribute('y', pad.t + h + 20);
|
1179 |
+
lbl.setAttribute('text-anchor', 'middle');
|
1180 |
+
lbl.setAttribute('font-size', '12');
|
1181 |
+
lbl.textContent = d.label;
|
1182 |
+
svg.appendChild(lbl);
|
1183 |
+
|
1184 |
+
const val = document.createElementNS(svg.namespaceURI, 'text');
|
1185 |
+
val.setAttribute('x', x + bw/2);
|
1186 |
+
val.setAttribute('y', y - 6);
|
1187 |
+
val.setAttribute('text-anchor', 'middle');
|
1188 |
+
val.setAttribute('font-size', '12');
|
1189 |
+
val.textContent = d.value.toFixed(2) + '%';
|
1190 |
+
svg.appendChild(val);
|
1191 |
+
});
|
1192 |
+
|
1193 |
+
state.charts[containerId] = svg;
|
1194 |
+
}
|
1195 |
+
|
1196 |
+
// Chart: Dot plot (forecasts)
|
1197 |
+
function drawDotPlot(containerId, series, opts={}) {
|
1198 |
+
const el = document.getElementById(containerId); el.innerHTML = '';
|
1199 |
+
const svg = createSVG(800, 480); el.appendChild(svg);
|
1200 |
+
const pad = {l: 70, r: 20, t: 20, b: 60};
|
1201 |
+
const w = 800 - pad.l - pad.r, h = 480 - pad.t - pad.b;
|
1202 |
+
const max = Math.max(opts.max || 9, ...series.map(d => d.value));
|
1203 |
+
const min = opts.min || 0;
|
1204 |
+
const filtered = series.filter(d => d.value >= (opts.threshold ?? 0));
|
1205 |
+
// axis
|
1206 |
+
axis(svg, pad.l, pad.t, w, h, 6, max, v => (v).toFixed(0) + '%');
|
1207 |
+
// baseline 0
|
1208 |
+
const x0 = pad.l, yMid = pad.t + h;
|
1209 |
+
// x scale
|
1210 |
+
const xScale = v => pad.l + (v - min) / (max - min) * w;
|
1211 |
+
|
1212 |
+
// categories y positions
|
1213 |
+
const items = filtered;
|
1214 |
+
const gap = h / (items.length + 1);
|
1215 |
+
|
1216 |
+
items.forEach((d, i) => {
|
1217 |
+
const y = pad.t + gap * (i+1);
|
1218 |
+
const x = xScale(d.value);
|
1219 |
+
const line = document.createElementNS(svg.namespaceURI, 'line');
|
1220 |
+
line.setAttribute('x1', pad.l); line.setAttribute('x2', pad.l + w); line.setAttribute('y1', y); line.setAttribute('y2', y);
|
1221 |
+
line.setAttribute('stroke', 'currentColor'); line.setAttribute('opacity', '.08');
|
1222 |
+
svg.appendChild(line);
|
1223 |
+
|
1224 |
+
const dot = document.createElementNS(svg.namespaceURI, 'circle');
|
1225 |
+
dot.setAttribute('cx', x); dot.setAttribute('cy', y); dot.setAttribute('r', '8');
|
1226 |
+
dot.setAttribute('fill', d.type === 'gov' ? 'var(--accent-2)' : 'var(--accent-3)');
|
1227 |
+
dot.setAttribute('opacity', '.9');
|
1228 |
+
svg.appendChild(dot);
|
1229 |
+
|
1230 |
+
const label = document.createElementNS(svg.namespaceURI, 'text');
|
1231 |
+
label.setAttribute('x', pad.l - 10); label.setAttribute('y', y + 4);
|
1232 |
+
label.setAttribute('text-anchor', 'end'); label.setAttribute('font-size', '12'); label.textContent = d.name;
|
1233 |
+
svg.appendChild(label);
|
1234 |
+
|
1235 |
+
const val = document.createElementNS(svg.namespaceURI, 'text');
|
1236 |
+
val.setAttribute('x', x + 10); val.setAttribute('y', y + 4); val.setAttribute('font-size','12'); val.textContent = d.value.toFixed(1) + '%';
|
1237 |
+
svg.appendChild(val);
|
1238 |
+
});
|
1239 |
+
state.charts[containerId] = svg;
|
1240 |
+
}
|
1241 |
+
|
1242 |
+
// Chart: Line
|
1243 |
+
function drawLine(containerId, series, opts={}) {
|
1244 |
+
const el = document.getElementById(containerId); el.innerHTML = '';
|
1245 |
+
const svg = createSVG(800, 480); el.appendChild(svg);
|
1246 |
+
const pad = {l: 60, r: 20, t: 20, b: 60};
|
1247 |
+
const w = 800 - pad.l - pad.r, h = 480 - pad.t - pad.b;
|
1248 |
+
const max = Math.max(opts.max || Math.max(...series.map(d=>d.value)) + 1, ...series.map(d => d.value));
|
1249 |
+
const min = Math.min(opts.min || Math.min(...series.map(d=>d.value)) - 0.5, ...series.map(d => d.value));
|
1250 |
+
axis(svg, pad.l, pad.t, w, h, 6, max, v => (v).toFixed(0) + '%');
|
1251 |
+
|
1252 |
+
const xScale = (i) => pad.l + (i / (series.length - 1)) * w;
|
1253 |
+
const yScale = (v) => pad.t + h - ((v - 0) / (max - 0)) * h;
|
1254 |
+
|
1255 |
+
const path = document.createElementNS(svg.namespaceURI, 'path');
|
1256 |
+
const d = series.map((p, i) => `${i===0?'M':'L'} ${xScale(i)} ${yScale(p.value)}`).join(' ');
|
1257 |
+
path.setAttribute('d', d);
|
1258 |
+
path.setAttribute('fill', 'none'); path.setAttribute('stroke', 'var(--accent)'); path.setAttribute('stroke-width', '3');
|
1259 |
+
svg.appendChild(path);
|
1260 |
+
|
1261 |
+
// points
|
1262 |
+
series.forEach((p, i) => {
|
1263 |
+
const c = document.createElementNS(svg.namespaceURI, 'circle');
|
1264 |
+
c.setAttribute('cx', xScale(i)); c.setAttribute('cy', yScale(p.value)); c.setAttribute('r', '5');
|
1265 |
+
c.setAttribute('fill', 'var(--accent)'); svg.appendChild(c);
|
1266 |
+
|
1267 |
+
const label = document.createElementNS(svg.namespaceURI, 'text');
|
1268 |
+
label.setAttribute('x', xScale(i)); label.setAttribute('y', pad.t + h + 20);
|
1269 |
+
label.setAttribute('text-anchor', 'middle'); label.setAttribute('font-size','12'); label.textContent = p.year;
|
1270 |
+
svg.appendChild(label);
|
1271 |
+
|
1272 |
+
const val = document.createElementNS(svg.namespaceURI, 'text');
|
1273 |
+
val.setAttribute('x', xScale(i)); val.setAttribute('y', yScale(p.value) - 8);
|
1274 |
+
val.setAttribute('text-anchor','middle'); val.setAttribute('font-size','12'); val.textContent = p.value.toFixed(2) + '%';
|
1275 |
+
svg.appendChild(val);
|
1276 |
+
});
|
1277 |
+
|
1278 |
+
// trendline
|
1279 |
+
const n = series.length;
|
1280 |
+
const meanX = (n - 1) / 2;
|
1281 |
+
const meanY = series.reduce((a, b) => a + b.value, 0) / n;
|
1282 |
+
let num = 0, den = 0;
|
1283 |
+
series.forEach((p, i) => { num += (i - meanX) * (p.value - meanY); den += (i - meanX) ** 2; });
|
1284 |
+
const slope = num / den; const intercept = meanY - slope * meanX;
|
1285 |
+
const y1 = intercept; const y2 = slope * (n-1) + intercept;
|
1286 |
+
const line = document.createElementNS(svg.namespaceURI, 'line');
|
1287 |
+
line.setAttribute('x1', xScale(0)); line.setAttribute('x2', xScale(n-1));
|
1288 |
+
line.setAttribute('y1', yScale(y1)); line.setAttribute('y2', yScale(y2));
|
1289 |
+
line.setAttribute('stroke', 'var(--accent-2)'); line.setAttribute('stroke-width', '2'); line.setAttribute('stroke-dasharray','6,6');
|
1290 |
+
svg.appendChild(line);
|
1291 |
+
|
1292 |
+
state.charts[containerId] = svg;
|
1293 |
+
}
|
1294 |
+
|
1295 |
+
// Chart: Mixed bars (FDI) and labels
|
1296 |
+
function drawFDI(containerId, series) {
|
1297 |
+
const el = document.getElementById(containerId); el.innerHTML = '';
|
1298 |
+
const svg = createSVG(800, 480); el.appendChild(svg);
|
1299 |
+
const pad = {l: 70, r: 20, t: 30, b: 80};
|
1300 |
+
const w = 800 - pad.l - pad.r, h = 480 - pad.t - pad.b;
|
1301 |
+
const max = Math.max(...series.map(d => d.value)) + 2;
|
1302 |
+
|
1303 |
+
axis(svg, pad.l, pad.t, w, h, 6, max, v => '$' + v.toFixed(0) + 'B');
|
1304 |
+
|
1305 |
+
const bw = w / (series.length * 1.6);
|
1306 |
+
series.forEach((d, i) => {
|
1307 |
+
const x = pad.l + i * (w / series.length) + (w / series.length - bw) / 2;
|
1308 |
+
const y = pad.t + h - (d.value / max) * h;
|
1309 |
+
const rect = document.createElementNS(svg.namespaceURI, 'rect');
|
1310 |
+
rect.setAttribute('x', x); rect.setAttribute('y', y);
|
1311 |
+
rect.setAttribute('width', bw); rect.setAttribute('height', Math.max(2, pad.t + h - y));
|
1312 |
+
rect.setAttribute('rx','6'); rect.setAttribute('fill', d.color || 'var(--accent)');
|
1313 |
+
svg.appendChild(rect);
|
1314 |
+
|
1315 |
+
const lbl = document.createElementNS(svg.namespaceURI, 'text');
|
1316 |
+
lbl.setAttribute('x', x + bw/2); lbl.setAttribute('y', pad.t + h + 20);
|
1317 |
+
lbl.setAttribute('text-anchor','middle'); lbl.setAttribute('font-size','12'); lbl.textContent = d.label;
|
1318 |
+
svg.appendChild(lbl);
|
1319 |
+
|
1320 |
+
const val = document.createElementNS(svg.namespaceURI, 'text');
|
1321 |
+
val.setAttribute('x', x + bw/2); val.setAttribute('y', y - 6);
|
1322 |
+
val.setAttribute('text-anchor','middle'); val.setAttribute('font-size','12'); val.textContent = fmt.moneyB(d.value);
|
1323 |
+
svg.appendChild(val);
|
1324 |
+
});
|
1325 |
+
|
1326 |
+
state.charts[containerId] = svg;
|
1327 |
+
}
|
1328 |
+
|
1329 |
+
// Chart: Inflation line + forecast markers
|
1330 |
+
function drawInflation(containerId, actual, forecasts) {
|
1331 |
+
const el = document.getElementById(containerId); el.innerHTML = '';
|
1332 |
+
const svg = createSVG(800, 480); el.appendChild(svg);
|
1333 |
+
const pad = {l: 60, r: 20, t: 20, b: 60};
|
1334 |
+
const w = 800 - pad.l - pad.r, h = 480 - pad.t - pad.b;
|
1335 |
+
const allVals = [...actual.map(d=>d.value), ...forecasts.map(f=>f.value)];
|
1336 |
+
const max = Math.max(5, ...allVals) + 0.5;
|
1337 |
+
|
1338 |
+
axis(svg, pad.l, pad.t, w, h, 5, max, v => v.toFixed(0) + '%');
|
1339 |
+
|
1340 |
+
const xScale = (i) => pad.l + (i / (actual.length - 1)) * w;
|
1341 |
+
const yScale = (v) => pad.t + h - (v / max) * h;
|
1342 |
+
|
1343 |
+
const path = document.createElementNS(svg.namespaceURI, 'path');
|
1344 |
+
const d = actual.map((p, i) => `${i===0?'M':'L'} ${xScale(i)} ${yScale(p.value)}`).join(' ');
|
1345 |
+
path.setAttribute('d', d);
|
1346 |
+
path.setAttribute('fill', 'none'); path.setAttribute('stroke', 'var(--accent)'); path.setAttribute('stroke-width','3');
|
1347 |
+
svg.appendChild(path);
|
1348 |
+
|
1349 |
+
actual.forEach((p, i) => {
|
1350 |
+
const c = document.createElementNS(svg.namespaceURI, 'circle');
|
1351 |
+
c.setAttribute('cx', xScale(i)); c.setAttribute('cy', yScale(p.value)); c.setAttribute('r', '6');
|
1352 |
+
c.setAttribute('fill', 'var(--accent)'); svg.appendChild(c);
|
1353 |
+
const label = document.createElementNS(svg.namespaceURI, 'text');
|
1354 |
+
label.setAttribute('x', xScale(i)); label.setAttribute('y', pad.t + h + 20);
|
1355 |
+
label.setAttribute('text-anchor','middle'); label.setAttribute('font-size','12'); label.textContent = p.month;
|
1356 |
+
svg.appendChild(label);
|
1357 |
+
});
|
1358 |
+
|
1359 |
+
// Forecast markers at end
|
1360 |
+
forecasts.forEach((f, idx) => {
|
1361 |
+
const x = pad.l + w - (idx * 80);
|
1362 |
+
const y = yScale(f.value);
|
1363 |
+
const rect = document.createElementNS(svg.namespaceURI, 'rect');
|
1364 |
+
rect.setAttribute('x', x - 40); rect.setAttribute('y', y - 10);
|
1365 |
+
rect.setAttribute('width', 80); rect.setAttribute('height', 20);
|
1366 |
+
rect.setAttribute('rx', '6'); rect.setAttribute('fill', f.color || 'var(--accent-3)'); rect.setAttribute('opacity', '.15');
|
1367 |
+
svg.appendChild(rect);
|
1368 |
+
|
1369 |
+
const line = document.createElementNS(svg.namespaceURI, 'line');
|
1370 |
+
line.setAttribute('x1', x); line.setAttribute('x2', x); line.setAttribute('y1', y); line.setAttribute('y2', yScale(actual.at(-1).value));
|
1371 |
+
line.setAttribute('stroke', f.color || 'var(--accent-3)'); line.setAttribute('stroke-dasharray','4,4');
|
1372 |
+
svg.appendChild(line);
|
1373 |
+
|
1374 |
+
const dot = document.createElementNS(svg.namespaceURI, 'circle');
|
1375 |
+
dot.setAttribute('cx', x); dot.setAttribute('cy', y); dot.setAttribute('r', '6'); dot.setAttribute('fill', f.color || 'var(--accent-3)');
|
1376 |
+
svg.appendChild(dot);
|
1377 |
+
|
1378 |
+
const label = document.createElementNS(svg.namespaceURI, 'text');
|
1379 |
+
label.setAttribute('x', x); label.setAttribute('y', y - 10); label.setAttribute('text-anchor','middle'); label.setAttribute('font-size','12'); label.textContent = f.name + ': ' + f.value.toFixed(1) + '%';
|
1380 |
+
svg.appendChild(label);
|
1381 |
+
});
|
1382 |
+
|
1383 |
+
state.charts[containerId] = svg;
|
1384 |
+
}
|
1385 |
+
|
1386 |
+
// Init charts
|
1387 |
+
const initCharts = () => {
|
1388 |
+
drawBars('chart-gdp-bars', data.gdpBars, {targetBand: [8.3, 8.5]});
|
1389 |
+
drawDotPlot('chart-forecasts', data.forecast, {threshold: parseFloat(document.getElementById('forecastMin').value)});
|
1390 |
+
drawInflation('chart-inflation', data.inflation, data.inflationForecasts);
|
1391 |
+
drawLine('chart-labor', data.unemployment.map((d, i) => ({year: d.label, value: d.value})), {min: 0, max: 5});
|
1392 |
+
drawFDI('chart-fdi', data.fdi);
|
1393 |
+
drawLine('chart-history', data.historyQ1.map(d => ({year: d.year, value: d.value})), {min: 0, max: 9});
|
1394 |
+
};
|
1395 |
+
initCharts();
|
1396 |
+
|
1397 |
+
// Controls for charts
|
1398 |
+
document.getElementById('forecastMin').addEventListener('input', (e) => {
|
1399 |
+
document.getElementById('forecastMinVal').textContent = e.target.value + '%';
|
1400 |
+
drawDotPlot('chart-forecasts', data.forecast, {threshold: parseFloat(e.target.value)});
|
1401 |
+
});
|
1402 |
+
document.getElementById('toggleTargetBand').addEventListener('change', () => {
|
1403 |
+
drawBars('chart-gdp-bars', data.gdpBars, {targetBand: [8.3, 8.5]});
|
1404 |
+
});
|
1405 |
+
document.querySelectorAll('.export-chart').forEach(btn => btn.addEventListener('click', e => {
|
1406 |
+
const id = e.currentTarget.dataset.chart;
|
1407 |
+
const svg = state.charts[id];
|
1408 |
+
if (svg) exportSVGToPNG(svg, id + '.png');
|
1409 |
+
}));
|
1410 |
+
|
1411 |
+
// Clipboard for link to section
|
1412 |
+
document.querySelectorAll('.link-section').forEach(btn => btn.addEventListener('click', (e) => {
|
1413 |
+
const id = e.currentTarget.dataset.target;
|
1414 |
+
const url = location.origin + location.pathname + '#' + id;
|
1415 |
+
navigator.clipboard.writeText(url).then(() => toast('Section link copied'));
|
1416 |
+
}));
|
1417 |
+
|
1418 |
+
// KPI copy accessible via Enter
|
1419 |
+
document.querySelectorAll('.copy-kpi').forEach(btn => btn.addEventListener('keydown', (e) => {
|
1420 |
+
if (e.key === 'Enter') btn.click();
|
1421 |
+
}));
|
1422 |
+
|
1423 |
+
// Re-render charts on theme change to ensure color variables apply (SVG inline uses CSS vars)
|
1424 |
+
const observerTheme = new MutationObserver(() => {
|
1425 |
+
Object.values(state.charts).forEach(svg => {}); // no-op; CSS vars update automatically
|
1426 |
+
});
|
1427 |
+
observerTheme.observe(document.documentElement, {attributes: true, attributeFilter: ['data-theme']});
|
1428 |
+
|
1429 |
+
// Responsive redrawing on container resize
|
1430 |
+
let resizeTimer;
|
1431 |
+
window.addEventListener('resize', () => {
|
1432 |
+
clearTimeout(resizeTimer);
|
1433 |
+
resizeTimer = setTimeout(() => initCharts(), 150);
|
1434 |
+
});
|
1435 |
+
|
1436 |
+
// Feather icons in dynamically created toasts will not need replacement, but ensure all are set at load.
|
1437 |
+
</script>
|
1438 |
+
</body>
|
1439 |
+
</html>
|