Spaces:
Running
Running
Upload index.html with huggingface_hub
Browse files- index.html +1333 -19
index.html
CHANGED
@@ -1,19 +1,1333 @@
|
|
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">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8" />
|
5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
6 |
+
<meta name="theme-color" content="#0e3a5d" />
|
7 |
+
<title>Vietnam Economic Growth Report 2025 — Interactive Research Presentation</title>
|
8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Source+Serif+4:opsz,[email protected],400;8..60,600&display=swap" rel="stylesheet">
|
11 |
+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/src/css/icons.css">
|
12 |
+
<style>
|
13 |
+
:root{
|
14 |
+
--bg: #0a0f14;
|
15 |
+
--bg-elev: #0d1822;
|
16 |
+
--text: #e8eef5;
|
17 |
+
--muted: #b9c8d9;
|
18 |
+
--brand: #4cc9f0;
|
19 |
+
--brand-2:#72efdd;
|
20 |
+
--accent:#ffd166;
|
21 |
+
--ok:#00d68f;
|
22 |
+
--warn:#ff7a59;
|
23 |
+
--bad:#ff4d6d;
|
24 |
+
--card:#0f2232;
|
25 |
+
--border:#1a3346;
|
26 |
+
--shadow: 0 8px 24px rgba(0,0,0,.35);
|
27 |
+
--radius: 14px;
|
28 |
+
--radius-sm: 10px;
|
29 |
+
--radius-xs: 8px;
|
30 |
+
--space: clamp(16px, 2.5vw, 28px);
|
31 |
+
--lead: clamp(18px, 2.1vw, 22px);
|
32 |
+
--h1: clamp(28px, 5vw, 56px);
|
33 |
+
--h2: clamp(22px, 3vw, 34px);
|
34 |
+
--h3: clamp(18px, 2.4vw, 24px);
|
35 |
+
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
36 |
+
}
|
37 |
+
:root.light{
|
38 |
+
--bg:#f7fafc;
|
39 |
+
--bg-elev:#ffffff;
|
40 |
+
--text:#0b1a26;
|
41 |
+
--muted:#4c6275;
|
42 |
+
--brand:#1565c0;
|
43 |
+
--brand-2:#1ec8a5;
|
44 |
+
--accent:#b26a00;
|
45 |
+
--ok:#12805c;
|
46 |
+
--warn:#b93817;
|
47 |
+
--bad:#a10035;
|
48 |
+
--card:#ffffff;
|
49 |
+
--border:#e6eef5;
|
50 |
+
--shadow: 0 10px 24px rgba(10,26,38,.08);
|
51 |
+
}
|
52 |
+
*{box-sizing:border-box}
|
53 |
+
html{scroll-behavior:smooth}
|
54 |
+
body{
|
55 |
+
margin:0; background:var(--bg); color:var(--text);
|
56 |
+
font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
|
57 |
+
line-height:1.6;
|
58 |
+
}
|
59 |
+
a{color:var(--brand)}
|
60 |
+
.container{
|
61 |
+
display:grid; grid-template-columns: 1fr; gap:var(--space);
|
62 |
+
max-width: 1400px; margin-inline:auto; padding: calc(var(--space) + 16px) var(--space) var(--space);
|
63 |
+
}
|
64 |
+
header.appbar{
|
65 |
+
position: sticky; top:0; z-index:1000; background:linear-gradient(180deg, rgba(10,15,20,.92), rgba(10,15,20,.70));
|
66 |
+
-webkit-backdrop-filter:saturate(130%) blur(10px); backdrop-filter:saturate(130%) blur(10px);
|
67 |
+
border-bottom:1px solid var(--border);
|
68 |
+
}
|
69 |
+
.appbar-inner{
|
70 |
+
max-width:1400px; margin-inline:auto; display:flex; align-items:center; gap:12px; padding:10px var(--space);
|
71 |
+
}
|
72 |
+
.brand{
|
73 |
+
display:flex; align-items:center; gap:12px; padding:8px 12px; border-radius:12px;
|
74 |
+
}
|
75 |
+
.brand i{font-size:28px; color:var(--brand-2)}
|
76 |
+
.brand h1{font-size:clamp(16px,2vw,22px); margin:0}
|
77 |
+
.brand .subtitle{font-size:12px; color:var(--muted)}
|
78 |
+
.searchbar{
|
79 |
+
flex:1; display:flex; align-items:center; gap:8px;
|
80 |
+
background:var(--bg-elev); border:1px solid var(--border); padding:8px 12px; border-radius: 999px;
|
81 |
+
}
|
82 |
+
.searchbar i{color:var(--muted)}
|
83 |
+
.searchbar input{
|
84 |
+
flex:1; border:0; outline:0; background:transparent; color:var(--text);
|
85 |
+
font-size:14px;
|
86 |
+
}
|
87 |
+
.actions{display:flex; align-items:center; gap:8px}
|
88 |
+
button, .btn{
|
89 |
+
display:inline-flex; align-items:center; gap:8px; padding:10px 14px; border:1px solid var(--border);
|
90 |
+
background:var(--card); color:var(--text); border-radius:10px; cursor:pointer; box-shadow: var(--shadow);
|
91 |
+
}
|
92 |
+
button.ghost{background:transparent; box-shadow:none}
|
93 |
+
button.primary{background:linear-gradient(135deg,var(--brand),var(--brand-2)); border-color:transparent; color:#00131f}
|
94 |
+
button.icon{padding:10px; border-radius:10px; width:42px; height:42px; justify-content:center}
|
95 |
+
button:focus-visible{outline:2px solid var(--brand-2)}
|
96 |
+
.badge{display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:999px; background:rgba(114,239,221,.15); color:var(--brand-2); border:1px solid rgba(114,239,221,.35)}
|
97 |
+
kbd{font-family:var(--mono); background:var(--bg-elev); border:1px solid var(--border); border-bottom-width:3px; border-radius:6px; padding:0 6px; font-size:12px}
|
98 |
+
|
99 |
+
/* Layout */
|
100 |
+
.layout{
|
101 |
+
display:grid; grid-template-columns: 1fr; gap: var(--space);
|
102 |
+
}
|
103 |
+
aside.toc{
|
104 |
+
position: sticky; top: calc(68px + 8px); align-self:start; background:var(--card); border:1px solid var(--border);
|
105 |
+
border-radius: var(--radius); padding:16px; max-height: calc(100vh - 96px); overflow:auto;
|
106 |
+
}
|
107 |
+
.toc h3{margin:0 0 8px; font-size:14px; letter-spacing:.04em; text-transform:uppercase; color:var(--muted)}
|
108 |
+
.toc a{display:flex; align-items:center; gap:8px; padding:8px 10px; border-radius:8px; color:var(--text); text-decoration:none; border:1px solid transparent}
|
109 |
+
.toc a:hover{background:rgba(114,239,221,.06); border-color:var(--border)}
|
110 |
+
.toc a.active{background:linear-gradient(180deg, rgba(76,201,240,.15), rgba(76,201,240,.03)); border-color:rgba(76,201,240,.35)}
|
111 |
+
.toc .minor{padding-left:28px; font-size:13px; color:var(--muted)}
|
112 |
+
.mobile-toc-toggle{display:none}
|
113 |
+
|
114 |
+
main.content{
|
115 |
+
display:grid; gap: var(--space);
|
116 |
+
}
|
117 |
+
section{
|
118 |
+
background:var(--card); border:1px solid var(--border); border-radius: var(--radius);
|
119 |
+
padding: clamp(16px, 2.5vw, 28px);
|
120 |
+
scroll-margin-top: 96px;
|
121 |
+
container-type: inline-size; /* container queries */
|
122 |
+
container-name: section;
|
123 |
+
}
|
124 |
+
h2.section-title{
|
125 |
+
font-family:"Source Serif 4", Georgia, serif; font-weight:700; font-size:var(--h2); margin:0 0 10px;
|
126 |
+
}
|
127 |
+
.section-meta{display:flex; flex-wrap:wrap; gap:10px; color:var(--muted); font-size:14px}
|
128 |
+
.lead{font-size:var(--lead); color:var(--muted)}
|
129 |
+
.grid{
|
130 |
+
display:grid; gap:16px; grid-template-columns: 1fr;
|
131 |
+
}
|
132 |
+
.card{
|
133 |
+
background:var(--bg-elev); border:1px solid var(--border); border-radius: var(--radius-sm); padding:16px; box-shadow: var(--shadow);
|
134 |
+
}
|
135 |
+
.card h3{margin:0 0 8px; font-size:var(--h3)}
|
136 |
+
.card .desc{color:var(--muted); font-size:14px}
|
137 |
+
.insights{display:grid; gap:12px}
|
138 |
+
.insight{
|
139 |
+
display:flex; align-items:flex-start; gap:10px; font-size:14px; color:var(--text); background:linear-gradient(180deg, rgba(114,239,221,.12), transparent);
|
140 |
+
border:1px dashed rgba(114,239,221,.35); border-radius:10px; padding:10px 12px;
|
141 |
+
}
|
142 |
+
.insight i{color:var(--brand-2)}
|
143 |
+
.kpi{
|
144 |
+
display:grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap:12px;
|
145 |
+
}
|
146 |
+
.kpi .tile{
|
147 |
+
border:1px solid var(--border); border-radius:12px; padding:14px; display:flex; gap:12px; align-items:center; background:var(--card)
|
148 |
+
}
|
149 |
+
.tile i{font-size:22px}
|
150 |
+
.tile .val{font-weight:700; font-size: clamp(18px, 4vw, 26px)}
|
151 |
+
.tile .lbl{font-size:12px; color:var(--muted)}
|
152 |
+
.chart{
|
153 |
+
position:relative; display:grid; gap:10px;
|
154 |
+
}
|
155 |
+
.chart canvas{width:100%; height:auto; aspect-ratio: 16 / 9; background:linear-gradient(180deg, rgba(255,255,255,.02), transparent); border-radius:10px}
|
156 |
+
.chart-actions{display:flex; gap:8px; align-items:center; flex-wrap:wrap}
|
157 |
+
.legend{display:flex; gap:8px; flex-wrap:wrap}
|
158 |
+
.legend .key{display:flex; align-items:center; gap:6px; font-size:12px; color:var(--muted)}
|
159 |
+
.legend .swatch{width:10px; height:10px; border-radius:3px; display:inline-block; border:1px solid var(--border)}
|
160 |
+
.tooltip{
|
161 |
+
position:absolute; pointer-events:none; background:var(--bg-elev); border:1px solid var(--border); padding:8px 10px; border-radius:8px; font-size:12px; color:var(--text);
|
162 |
+
transform:translate(-50%,-120%); white-space:nowrap; display:none; z-index:2;
|
163 |
+
}
|
164 |
+
|
165 |
+
/* Table */
|
166 |
+
.table-wrap{overflow:auto; border:1px solid var(--border); border-radius:12px}
|
167 |
+
table{
|
168 |
+
width:100%; border-collapse:separate; border-spacing:0; font-size:14px; min-width:820px;
|
169 |
+
}
|
170 |
+
thead th{
|
171 |
+
position:sticky; top:0; background:var(--card); border-bottom:1px solid var(--border);
|
172 |
+
text-align:left; padding:12px; font-weight:600; z-index:1;
|
173 |
+
}
|
174 |
+
tbody td{padding:12px; border-bottom:1px solid var(--border)}
|
175 |
+
thead th.sortable{cursor:pointer}
|
176 |
+
thead th.sortable i{opacity:.6}
|
177 |
+
tr:hover td{background:rgba(114,239,221,.04)}
|
178 |
+
.status{display:inline-flex; align-items:center; gap:6px; font-size:12px; color:var(--muted)}
|
179 |
+
.tag{display:inline-flex; align-items:center; gap:6px; padding:4px 8px; border-radius:999px; border:1px solid var(--border); background:var(--bg-elev); font-size:12px}
|
180 |
+
.tags{display:flex; gap:8px; flex-wrap:wrap}
|
181 |
+
|
182 |
+
/* Collapsible (details) */
|
183 |
+
details{border:1px solid var(--border); border-radius:12px; padding:12px; background:var(--bg-elev)}
|
184 |
+
details summary{cursor:pointer; list-style:none; display:flex; align-items:center; gap:8px; font-weight:600}
|
185 |
+
details summary::-webkit-details-marker{display:none}
|
186 |
+
|
187 |
+
/* Progress indicator */
|
188 |
+
.progress{
|
189 |
+
position:fixed; top:0; left:0; height:4px; width:100%; background:linear-gradient(90deg, var(--brand), var(--brand-2));
|
190 |
+
transform-origin:0 50%; transform:scaleX(0); z-index:1100;
|
191 |
+
}
|
192 |
+
|
193 |
+
/* Footer */
|
194 |
+
footer{
|
195 |
+
max-width:1400px; margin: 20px auto 60px; color:var(--muted); font-size:13px; padding-inline: var(--space);
|
196 |
+
}
|
197 |
+
.ref{display:flex; gap:10px; align-items:flex-start; padding:10px 0; border-bottom:1px dashed var(--border)}
|
198 |
+
.ref:last-child{border-bottom:0}
|
199 |
+
.ref a{word-break:break-all}
|
200 |
+
|
201 |
+
/* Responsive */
|
202 |
+
@media (min-width: 768px){
|
203 |
+
.layout{
|
204 |
+
grid-template-columns: 280px 1fr;
|
205 |
+
}
|
206 |
+
.kpi{grid-template-columns: repeat(4, minmax(0,1fr))}
|
207 |
+
.grid.cols-2{grid-template-columns: repeat(2, minmax(0,1fr))}
|
208 |
+
}
|
209 |
+
@media (min-width: 1024px){
|
210 |
+
.grid.cols-3{grid-template-columns: repeat(3, minmax(0,1fr))}
|
211 |
+
.grid.cols-4{grid-template-columns: repeat(4, minmax(0,1fr))}
|
212 |
+
}
|
213 |
+
@media (min-width: 1440px){
|
214 |
+
.layout{grid-template-columns: 320px 1fr}
|
215 |
+
}
|
216 |
+
|
217 |
+
/* Container queries for chart/meta layout inside sections */
|
218 |
+
@container section (min-width: 700px){
|
219 |
+
.section-grid{display:grid; grid-template-columns: 1.2fr .8fr; gap:16px}
|
220 |
+
}
|
221 |
+
|
222 |
+
/* Utilities */
|
223 |
+
.small{font-size:12px}
|
224 |
+
.muted{color:var(--muted)}
|
225 |
+
.hr{height:1px; background:var(--border); margin:12px 0}
|
226 |
+
.right{margin-left:auto}
|
227 |
+
.nowrap{white-space:nowrap}
|
228 |
+
.sticky-banner{
|
229 |
+
position:fixed; right:16px; bottom:16px; background:var(--bg-elev); border:1px solid var(--border); border-radius:12px; padding:10px 12px; display:flex; gap:8px; align-items:center; box-shadow: var(--shadow)
|
230 |
+
}
|
231 |
+
.hidden{display:none !important}
|
232 |
+
.highlight{background:linear-gradient(180deg, rgba(255,209,102,.5), rgba(255,209,102,.2)); border-radius:4px}
|
233 |
+
hr.soft{border:0; height:1px; background:var(--border); margin:12px 0}
|
234 |
+
|
235 |
+
/* Mobile TOC */
|
236 |
+
@media (max-width: 767.98px){
|
237 |
+
.mobile-toc-toggle{display:inline-flex}
|
238 |
+
aside.toc{display:none}
|
239 |
+
.toc-drawer{
|
240 |
+
position:fixed; inset:0; z-index:1050; display:none; background:rgba(0,0,0,.5);
|
241 |
+
}
|
242 |
+
.toc-panel{
|
243 |
+
position:absolute; left:0; top:0; bottom:0; width:min(86vw, 360px); background:var(--card); border-right:1px solid var(--border); padding:16px; overflow:auto;
|
244 |
+
}
|
245 |
+
}
|
246 |
+
|
247 |
+
/* Callouts */
|
248 |
+
.callout{
|
249 |
+
display:flex; gap:10px; padding:12px; border-radius:10px; border:1px solid var(--border);
|
250 |
+
}
|
251 |
+
.callout.ok{background:rgba(0,214,143,.08); border-color:rgba(0,214,143,.35)}
|
252 |
+
.callout.warn{background:rgba(255,122,89,.08); border-color:rgba(255,122,89,.35)}
|
253 |
+
.callout i{font-size:18px}
|
254 |
+
</style>
|
255 |
+
</head>
|
256 |
+
<body>
|
257 |
+
<div class="progress" id="progress"></div>
|
258 |
+
<header class="appbar" role="banner">
|
259 |
+
<div class="appbar-inner">
|
260 |
+
<button class="icon mobile-toc-toggle" id="openTOC" aria-label="Open table of contents"><i class="ph ph-list"></i></button>
|
261 |
+
<div class="brand">
|
262 |
+
<i class="ph ph-chart-line-up"></i>
|
263 |
+
<div>
|
264 |
+
<h1>Vietnam Economic Growth Report 2025</h1>
|
265 |
+
<div class="subtitle">Interactive research presentation with data, charts, and citations</div>
|
266 |
+
</div>
|
267 |
+
</div>
|
268 |
+
<div class="searchbar" role="search">
|
269 |
+
<i class="ph ph-magnifying-glass"></i>
|
270 |
+
<input id="searchInput" type="search" placeholder="Search the report (press / to focus)..." aria-label="Search" />
|
271 |
+
<span class="badge"><i class="ph ph-command"></i><kbd>/</kbd></span>
|
272 |
+
</div>
|
273 |
+
<div class="actions">
|
274 |
+
<button class="icon ghost" id="themeToggle" aria-label="Toggle theme"><i class="ph ph-moon-stars"></i></button>
|
275 |
+
<button class="icon ghost" id="shareReport" aria-label="Copy report link"><i class="ph ph-link-simple"></i></button>
|
276 |
+
<button class="primary" id="printReport"><i class="ph ph-file-arrow-down"></i> Export PDF</button>
|
277 |
+
</div>
|
278 |
+
</div>
|
279 |
+
</header>
|
280 |
+
|
281 |
+
<div class="container">
|
282 |
+
<div class="layout">
|
283 |
+
<aside class="toc" id="toc" aria-label="Table of contents">
|
284 |
+
<h3>Contents</h3>
|
285 |
+
<nav>
|
286 |
+
<a href="#exec" class="toc-link"><i class="ph ph-sparkle"></i> Executive Summary</a>
|
287 |
+
<a href="#methodology" class="toc-link"><i class="ph ph-flask"></i> Methodology</a>
|
288 |
+
<a href="#indicators" class="toc-link"><i class="ph ph-gauge"></i> Key Indicators</a>
|
289 |
+
<a href="#sector" class="toc-link"><i class="ph ph-buildings"></i> Sectoral Analysis</a>
|
290 |
+
<a href="#challenges" class="toc-link"><i class="ph ph-warning"></i> Challenges & Risks</a>
|
291 |
+
<a href="#history" class="toc-link"><i class="ph ph-timer"></i> Historical Comparison</a>
|
292 |
+
<a href="#outlook" class="toc-link"><i class="ph ph-compass"></i> Outlook & Projections</a>
|
293 |
+
<a href="#data" class="toc-link minor"><i class="ph ph-table"></i> Data Explorer</a>
|
294 |
+
<a href="#conclusion" class="toc-link"><i class="ph ph-check-circle"></i> Conclusion</a>
|
295 |
+
<a href="#references" class="toc-link"><i class="ph ph-bookmarks"></i> Sources & Citations</a>
|
296 |
+
<a href="#appendix" class="toc-link"><i class="ph ph-note"></i> Appendices</a>
|
297 |
+
</nav>
|
298 |
+
<div class="hr"></div>
|
299 |
+
<div class="small muted">Tip: Click any section title to copy a deep link.</div>
|
300 |
+
</aside>
|
301 |
+
|
302 |
+
<main class="content" id="content">
|
303 |
+
|
304 |
+
<section id="exec" data-title="Executive Summary">
|
305 |
+
<div class="section-grid">
|
306 |
+
<div>
|
307 |
+
<h2 class="section-title">Executive Summary <button class="icon ghost small copy-link" aria-label="Copy link to section"><i class="ph ph-link"></i></button></h2>
|
308 |
+
<p class="lead">Vietnam’s economy maintained robust momentum in 2025. GDP grew 6.9% YoY in Q1 and 7.96% in Q2; first-half growth reached 7.52% — the strongest H1 since 2011, driven by services and manufacturing amid global trade tensions and US tariffs [<a href="#ref-1">1</a>, <a href="#ref-4">4</a>]. Inflation stayed contained within a 3–4.5% target range, unemployment remained low at 2.20% in Q1 2025, and FDI inflows accelerated, underscoring investor confidence [<a href="#ref-2">2</a>, <a href="#ref-8">8</a>, <a href="#ref-13">13</a>].</p>
|
309 |
+
<div class="insights">
|
310 |
+
<div class="insight"><i class="ph ph-trend-up"></i> H1 2025 GDP growth at 7.52% marks the highest mid-year pace since 2011 [<a href="#ref-4">4</a>].</div>
|
311 |
+
<div class="insight"><i class="ph ph-target"></i> Government’s 2025 growth target (8.3–8.5%) exceeds multilateral forecasts (ADB 6.6%, WB 5.8%, IMF 5.2%), implying upside ambition with execution risk [<a href="#ref-2">2</a>, <a href="#ref-16">16</a>].</div>
|
312 |
+
<div class="insight"><i class="ph ph-bank"></i> FDI momentum remains strong: US$18.4bn registered (+51% YoY) and US$8.9bn disbursed in the first five months; H1 total US$21.51bn (+32.6% YoY) [<a href="#ref-12">12</a>, <a href="#ref-13">13</a>].</div>
|
313 |
+
<div class="insight"><i class="ph ph-shopping-cart"></i> Retail sales in Q1 2025 reached VND 1.708 quadrillion (~US$66.8bn), up 9.9% YoY, signaling resilient domestic demand [<a href="#ref-4">4</a>].</div>
|
314 |
+
</div>
|
315 |
+
</div>
|
316 |
+
<div class="card">
|
317 |
+
<h3>Key Performance Snapshot</h3>
|
318 |
+
<div class="kpi">
|
319 |
+
<div class="tile"><i class="ph ph-graph"></i><div><div class="val">7.96%</div><div class="lbl">GDP Growth Q2 2025 [<a href="#ref-1">1</a>]</div></div></div>
|
320 |
+
<div class="tile"><i class="ph ph-graph"></i><div><div class="val">7.52%</div><div class="lbl">GDP Growth H1 2025 [<a href="#ref-4">4</a>]</div></div></div>
|
321 |
+
<div class="tile"><i class="ph ph-percent"></i><div><div class="val">3.57%</div><div class="lbl">Inflation (Jun 2025) [<a href="#ref-4">4</a>]</div></div></div>
|
322 |
+
<div class="tile"><i class="ph ph-users-three"></i><div><div class="val">2.20%</div><div class="lbl">Unemployment (Q1 2025) [<a href="#ref-4">4</a>]</div></div></div>
|
323 |
+
</div>
|
324 |
+
<div class="hr"></div>
|
325 |
+
<div class="tags">
|
326 |
+
<span class="tag"><i class="ph ph-shield-check"></i> Macro stability</span>
|
327 |
+
<span class="tag"><i class="ph ph-arrows-out-cardinal"></i> Resilient trade</span>
|
328 |
+
<span class="tag"><i class="ph ph-currency-circle-dollar"></i> FDI inflows</span>
|
329 |
+
</div>
|
330 |
+
</div>
|
331 |
+
</div>
|
332 |
+
</section>
|
333 |
+
|
334 |
+
<section id="methodology" data-title="Methodology">
|
335 |
+
<h2 class="section-title">Methodology <button class="icon ghost small copy-link" aria-label="Copy link to section"><i class="ph ph-link"></i></button></h2>
|
336 |
+
<div class="grid cols-2">
|
337 |
+
<div class="card">
|
338 |
+
<h3>Data Sources</h3>
|
339 |
+
<p class="desc">Primary statistics are drawn from Vietnam’s General Statistics Office (GSO) and Ministry of Planning and Investment, with cross-validation against IMF, ADB, and World Bank publications. FDI and macro indicator time series are benchmarked to Trading Economics and Vietnam Investment Review where applicable [<a href="#ref-2">2</a>, <a href="#ref-4">4</a>, <a href="#ref-8">8</a>, <a href="#ref-12">12</a>, <a href="#ref-13">13</a>, <a href="#ref-16">16</a>].</p>
|
340 |
+
<ul>
|
341 |
+
<li>GDP, inflation, labor market: GSO, IMF, ADB, WB</li>
|
342 |
+
<li>FDI inflows: MPI, VIR, Trading Economics</li>
|
343 |
+
<li>Retail sales: GSO</li>
|
344 |
+
</ul>
|
345 |
+
</div>
|
346 |
+
<div class="card">
|
347 |
+
<h3>Compilation & Processing</h3>
|
348 |
+
<ul>
|
349 |
+
<li>Standardize growth metrics to YoY percentages.</li>
|
350 |
+
<li>Forecast benchmarks presented alongside actuals; government targets shown as range midpoint with proper notation.</li>
|
351 |
+
<li>Visualizations implemented via Canvas (no external JS frameworks). Datasets exported as CSV for reproducibility.</li>
|
352 |
+
</ul>
|
353 |
+
<div class="callout ok"><i class="ph ph-shield-check"></i> All figures are directly sourced from the provided report text and cited sources; no additional estimates were introduced.</div>
|
354 |
+
</div>
|
355 |
+
</div>
|
356 |
+
<details>
|
357 |
+
<summary><i class="ph ph-list-magnifying-glass"></i> Quality assurance checks</summary>
|
358 |
+
<ul>
|
359 |
+
<li>Cross-reference of GDP growth metrics across sections for consistency.</li>
|
360 |
+
<li>Unit harmonization (%, US$, VND) and explicit labeling.</li>
|
361 |
+
<li>Source tagging for each KPI and dataset row.</li>
|
362 |
+
</ul>
|
363 |
+
</details>
|
364 |
+
</section>
|
365 |
+
|
366 |
+
<section id="indicators" data-title="Key Indicators">
|
367 |
+
<h2 class="section-title">Key Economic Indicators 2025 <button class="icon ghost small copy-link" aria-label="Copy link to section"><i class="ph ph-link"></i></button></h2>
|
368 |
+
<p class="lead">Growth outpaces peers while inflation is contained. Forecasts by multilaterals are below the government’s target range [<a href="#ref-2">2</a>, <a href="#ref-16">16</a>].</p>
|
369 |
+
<div class="grid cols-2">
|
370 |
+
<div class="card chart" data-chart="bar" id="chart-gdp-2025">
|
371 |
+
<h3>2025 Growth: Actuals vs Forecasts</h3>
|
372 |
+
<div class="desc">Comparing quarterly and H1 actuals with year-end GDP forecasts. Government target is displayed at midpoint of the 8.3–8.5% range [<a href="#ref-2">2</a>, <a href="#ref-16">16</a>].</div>
|
373 |
+
<canvas aria-label="Bar chart: Vietnam GDP growth"></canvas>
|
374 |
+
<div class="tooltip"></div>
|
375 |
+
<div class="legend" id="legend-gdp-2025"></div>
|
376 |
+
<div class="chart-actions">
|
377 |
+
<button class="btn export-png"><i class="ph ph-image"></i> Export PNG</button>
|
378 |
+
<button class="btn copy-data"><i class="ph ph-clipboard"></i> Copy Data</button>
|
379 |
+
</div>
|
380 |
+
</div>
|
381 |
+
|
382 |
+
<div class="card chart" data-chart="line" id="chart-q1-2020-2025">
|
383 |
+
<h3>Q1 YoY GDP Growth (2020–2025)</h3>
|
384 |
+
<div class="desc">Year-on-year growth in Q1 across 2020–2025: 3.21, 4.85, 5.42, 3.46, 5.98, 6.93% [<a href="#ref-1">1</a>, <a href="#ref-4">4</a>].</div>
|
385 |
+
<canvas aria-label="Line chart: Q1 YoY GDP growth 2020–2025"></canvas>
|
386 |
+
<div class="tooltip"></div>
|
387 |
+
<div class="legend" id="legend-q1"></div>
|
388 |
+
<div class="chart-actions">
|
389 |
+
<button class="btn export-png"><i class="ph ph-image"></i> Export PNG</button>
|
390 |
+
<button class="btn copy-data"><i class="ph ph-clipboard"></i> Copy Data</button>
|
391 |
+
</div>
|
392 |
+
</div>
|
393 |
+
|
394 |
+
<div class="card chart" data-chart="donut" id="chart-fdi-2025">
|
395 |
+
<h3>FDI Composition (First 5 months, 2025)</h3>
|
396 |
+
<div class="desc">Registered: US$18.4bn; Disbursed: US$8.9bn. H1 total FDI reached US$21.51bn (+32.6% YoY) [<a href="#ref-12">12</a>, <a href="#ref-13">13</a>].</div>
|
397 |
+
<canvas aria-label="Donut chart: FDI composition"></canvas>
|
398 |
+
<div class="tooltip"></div>
|
399 |
+
<div class="legend" id="legend-fdi"></div>
|
400 |
+
<div class="chart-actions">
|
401 |
+
<button class="btn export-png"><i class="ph ph-image"></i> Export PNG</button>
|
402 |
+
<button class="btn copy-data"><i class="ph ph-clipboard"></i> Copy Data</button>
|
403 |
+
</div>
|
404 |
+
</div>
|
405 |
+
|
406 |
+
<div class="card chart" data-chart="heatmap" id="chart-forecasts">
|
407 |
+
<h3>Forecast Heatmap (2025)</h3>
|
408 |
+
<div class="desc">GDP and inflation forecasts from IMF, ADB, and World Bank; government target shown for GDP only [<a href="#ref-2">2</a>, <a href="#ref-16">16</a>].</div>
|
409 |
+
<canvas aria-label="Heatmap: Forecasts"></canvas>
|
410 |
+
<div class="tooltip"></div>
|
411 |
+
<div class="legend" id="legend-heatmap"></div>
|
412 |
+
<div class="chart-actions">
|
413 |
+
<button class="btn export-png"><i class="ph ph-image"></i> Export PNG</button>
|
414 |
+
<button class="btn copy-data"><i class="ph ph-clipboard"></i> Copy Data</button>
|
415 |
+
</div>
|
416 |
+
</div>
|
417 |
+
</div>
|
418 |
+
|
419 |
+
<div class="insights" style="margin-top:12px">
|
420 |
+
<div class="insight"><i class="ph ph-seal-check"></i> Inflation remains within the manageable 3–4.5% range (3.24% in May; 3.57% in June) [<a href="#ref-4">4</a>].</div>
|
421 |
+
<div class="insight"><i class="ph ph-trend-up"></i> Forecast dispersion highlights conservative external views relative to domestic targets (IMF 5.2%, WB 5.8%, ADB 6.6% vs government 8.3–8.5%) [<a href="#ref-2">2</a>, <a href="#ref-16">16</a>].</div>
|
422 |
+
</div>
|
423 |
+
</section>
|
424 |
+
|
425 |
+
<section id="sector" data-title="Sectoral Analysis">
|
426 |
+
<h2 class="section-title">Sectoral Analysis <button class="icon ghost small copy-link" aria-label="Copy link to section"><i class="ph ph-link"></i></button></h2>
|
427 |
+
<div class="grid cols-2">
|
428 |
+
<div class="card">
|
429 |
+
<h3>Primary Growth Drivers</h3>
|
430 |
+
<ul>
|
431 |
+
<li><strong>Services</strong>: Major contributor to GDP growth [<a href="#ref-4">4</a>].</li>
|
432 |
+
<li><strong>Manufacturing</strong>: Sustains recovery trajectory [<a href="#ref-4">4</a>].</li>
|
433 |
+
<li><strong>Exports</strong>: Continue to anchor growth despite headwinds [<a href="#ref-4">4</a>].</li>
|
434 |
+
<li><strong>Banking</strong>: Earnings projected +17% in 2025 on credit growth (+15%) [<a href="#ref-15">15</a>].</li>
|
435 |
+
</ul>
|
436 |
+
<div class="callout ok"><i class="ph ph-trend-up"></i> Retail sales in Q1 2025 rose 9.9% YoY to VND 1.708 quadrillion (~US$66.83bn), reinforcing domestic demand strength [<a href="#ref-4">4</a>].</div>
|
437 |
+
</div>
|
438 |
+
<div class="card">
|
439 |
+
<h3>Context & Sensitivities</h3>
|
440 |
+
<p class="desc">Global trade tensions and tariffs present downside risks to export-oriented sectors, yet Vietnam’s competitiveness and FDI pipeline provide buffers [<a href="#ref-9">9</a>, <a href="#ref-13">13</a>].</p>
|
441 |
+
<ul class="tags">
|
442 |
+
<li class="tag"><i class="ph ph-ship"></i> Trade</li>
|
443 |
+
<li class="tag"><i class="ph ph-factory"></i> Manufacturing</li>
|
444 |
+
<li class="tag"><i class="ph ph-bank"></i> Banking</li>
|
445 |
+
<li class="tag"><i class="ph ph-storefront"></i> Retail</li>
|
446 |
+
</ul>
|
447 |
+
</div>
|
448 |
+
</div>
|
449 |
+
</section>
|
450 |
+
|
451 |
+
<section id="challenges" data-title="Challenges and Risk Factors">
|
452 |
+
<h2 class="section-title">Challenges & Risk Factors <button class="icon ghost small copy-link" aria-label="Copy link to section"><i class="ph ph-link"></i></button></h2>
|
453 |
+
<div class="grid">
|
454 |
+
<div class="card">
|
455 |
+
<h3>Key Risks</h3>
|
456 |
+
<ol>
|
457 |
+
<li>Global trade tensions impacting exports [<a href="#ref-9">9</a>].</li>
|
458 |
+
<li>US tariff policies pressuring exporters [<a href="#ref-9">9</a>].</li>
|
459 |
+
<li>Geopolitical instability elevating uncertainty [<a href="#ref-14">14</a>].</li>
|
460 |
+
<li>Potential overdependence on FDI; inflation vigilance advised [<a href="#ref-15">15</a>].</li>
|
461 |
+
<li>Safeguarding macro stability (debt, inflation control) [<a href="#ref-2">2</a>].</li>
|
462 |
+
</ol>
|
463 |
+
<div class="callout warn"><i class="ph ph-warning"></i> Policy stance should balance growth support with macroprudential safeguards and targeted social buffers [<a href="#ref-2">2</a>].</div>
|
464 |
+
</div>
|
465 |
+
<div class="card">
|
466 |
+
<h3>Risk Mitigation Strategies</h3>
|
467 |
+
<ul>
|
468 |
+
<li>Diversify export markets and products.</li>
|
469 |
+
<li>Strengthen domestic demand via targeted fiscal measures.</li>
|
470 |
+
<li>Enhance economic resilience, maintain policy space.</li>
|
471 |
+
<li>Maintain macro stability and monitor leverage/inflation [<a href="#ref-2">2</a>].</li>
|
472 |
+
</ul>
|
473 |
+
</div>
|
474 |
+
</div>
|
475 |
+
</section>
|
476 |
+
|
477 |
+
<section id="history" data-title="Historical Comparison">
|
478 |
+
<h2 class="section-title">Historical Comparison <button class="icon ghost small copy-link" aria-label="Copy link to section"><i class="ph ph-link"></i></button></h2>
|
479 |
+
<div class="grid cols-2">
|
480 |
+
<div class="card">
|
481 |
+
<h3>Recent Performance</h3>
|
482 |
+
<ul>
|
483 |
+
<li>2024 GDP growth: 7.1% [<a href="#ref-1">1</a>].</li>
|
484 |
+
<li>2025 growth may moderate due to external factors; long-term fundamentals remain resilient [<a href="#ref-2">2</a>].</li>
|
485 |
+
<li>Q1 YoY GDP growth (2020–2025): 3.21; 4.85; 5.42; 3.46; 5.98; 6.93 (%) [<a href="#ref-1">1</a>, <a href="#ref-4">4</a>].</li>
|
486 |
+
</ul>
|
487 |
+
</div>
|
488 |
+
<div class="card">
|
489 |
+
<h3>Interpretation</h3>
|
490 |
+
<p class="desc">Post-pandemic recovery phases show normalization and renewed momentum through 2024–2025, with services and manufacturing as anchors while policy prudence contains inflation [<a href="#ref-2">2</a>, <a href="#ref-4">4</a>].</p>
|
491 |
+
</div>
|
492 |
+
</div>
|
493 |
+
</section>
|
494 |
+
|
495 |
+
<section id="outlook" data-title="Economic Outlook and Projections">
|
496 |
+
<h2 class="section-title">Outlook & Projections <button class="icon ghost small copy-link" aria-label="Copy link to section"><i class="ph ph-link"></i></button></h2>
|
497 |
+
<div class="grid cols-2">
|
498 |
+
<div class="card">
|
499 |
+
<h3>Near-term Prospects (2025)</h3>
|
500 |
+
<p>Growth is expected to remain solid despite uncertainty. The government’s 8.3–8.5% GDP target is ambitious relative to international forecasts, but domestic fundamentals (FDI, low unemployment, contained inflation) provide support [<a href="#ref-2">2</a>, <a href="#ref-16">16</a>].</p>
|
501 |
+
<ul>
|
502 |
+
<li>Robust FDI inflows signal confidence [<a href="#ref-12">12</a>].</li>
|
503 |
+
<li>Low unemployment supports consumption [<a href="#ref-4">4</a>].</li>
|
504 |
+
<li>Inflation control preserves purchasing power [<a href="#ref-2">2</a>].</li>
|
505 |
+
<li>Export competitiveness persists despite headwinds [<a href="#ref-9">9</a>].</li>
|
506 |
+
<li>Parliament support for higher growth target [<a href="#ref-16">16</a>].</li>
|
507 |
+
</ul>
|
508 |
+
</div>
|
509 |
+
<div class="card">
|
510 |
+
<h3>Policy Signals</h3>
|
511 |
+
<p class="desc">Authorities aim for ~8% growth in 2025 to pave the way for higher medium-term expansion while retaining fiscal space to cushion shocks if needed [<a href="#ref-2">2</a>].</p>
|
512 |
+
<div class="callout ok"><i class="ph ph-check-circle"></i> Policy mix: targeted fiscal support, export diversification, and structural reforms to raise productivity [<a href="#ref-2">2</a>, <a href="#ref-16">16</a>].</div>
|
513 |
+
</div>
|
514 |
+
</div>
|
515 |
+
</section>
|
516 |
+
|
517 |
+
<section id="data" data-title="Data Explorer">
|
518 |
+
<h2 class="section-title">Data Explorer <button class="icon ghost small copy-link" aria-label="Copy link to section"><i class="ph ph-link"></i></button></h2>
|
519 |
+
<div class="grid">
|
520 |
+
<div class="card">
|
521 |
+
<h3>Filter & Search</h3>
|
522 |
+
<div class="tags">
|
523 |
+
<label class="tag"><input type="checkbox" class="cat-filter" value="GDP" checked /> GDP</label>
|
524 |
+
<label class="tag"><input type="checkbox" class="cat-filter" value="Inflation" checked /> Inflation</label>
|
525 |
+
<label class="tag"><input type="checkbox" class="cat-filter" value="Unemployment" checked /> Unemployment</label>
|
526 |
+
<label class="tag"><input type="checkbox" class="cat-filter" value="FDI" checked /> FDI</label>
|
527 |
+
<label class="tag"><input type="checkbox" class="cat-filter" value="Retail" checked /> Retail</label>
|
528 |
+
<span class="right">
|
529 |
+
<button class="btn small" id="downloadCSV"><i class="ph ph-download-simple"></i> Download CSV</button>
|
530 |
+
</span>
|
531 |
+
</div>
|
532 |
+
<div class="table-wrap">
|
533 |
+
<table id="dataTable">
|
534 |
+
<thead>
|
535 |
+
<tr>
|
536 |
+
<th class="sortable">Category <i class="ph ph-arrows-down-up"></i></th>
|
537 |
+
<th class="sortable">Period <i class="ph ph-arrows-down-up"></i></th>
|
538 |
+
<th class="sortable">Indicator <i class="ph ph-arrows-down-up"></i></th>
|
539 |
+
<th class="sortable">Value <i class="ph ph-arrows-down-up"></i></th>
|
540 |
+
<th>Unit</th>
|
541 |
+
<th>Notes</th>
|
542 |
+
<th>Source</th>
|
543 |
+
</tr>
|
544 |
+
</thead>
|
545 |
+
<tbody>
|
546 |
+
<!-- rows injected by JS -->
|
547 |
+
</tbody>
|
548 |
+
</table>
|
549 |
+
</div>
|
550 |
+
</div>
|
551 |
+
</div>
|
552 |
+
</section>
|
553 |
+
|
554 |
+
<section id="conclusion" data-title="Conclusion">
|
555 |
+
<h2 class="section-title">Conclusion <button class="icon ghost small copy-link" aria-label="Copy link to section"><i class="ph ph-link"></i></button></h2>
|
556 |
+
<p class="lead">Vietnam’s 2025 performance showcases resilience and strong fundamentals. External risks persist, but macro stability, robust FDI, and low unemployment underpin a cautiously optimistic outlook. The gap between government targets and international forecasts warrants realism with continued reform momentum [<a href="#ref-2">2</a>, <a href="#ref-4">4</a>, <a href="#ref-16">16</a>].</p>
|
557 |
+
</section>
|
558 |
+
|
559 |
+
<section id="references" data-title="Sources and Citations">
|
560 |
+
<h2 class="section-title">Sources & Citations <button class="icon ghost small copy-link" aria-label="Copy link to section"><i class="ph ph-link"></i></button></h2>
|
561 |
+
<div class="ref" id="ref-1"><strong>1.</strong> Trading Economics - Vietnam GDP Annual Growth Rate — <a href="https://tradingeconomics.com/vietnam/gdp-growth-annual" target="_blank" rel="noopener">https://tradingeconomics.com/vietnam/gdp-growth-annual</a></div>
|
562 |
+
<div class="ref" id="ref-2"><strong>2.</strong> International Monetary Fund - Vietnam Country Profile — <a href="https://www.imf.org/en/Countries/VNM" target="_blank" rel="noopener">https://www.imf.org/en/Countries/VNM</a></div>
|
563 |
+
<div class="ref" id="ref-3"><strong>3.</strong> World Economics - Vietnam GDP Estimates — <a href="https://www.worldeconomics.com/GDP/Vietnam.gdp" target="_blank" rel="noopener">https://www.worldeconomics.com/GDP/Vietnam.gdp</a></div>
|
564 |
+
<div class="ref" id="ref-4"><strong>4.</strong> Government of Vietnam - General Statistics Office — <a href="https://www.gso.gov.vn/en/" target="_blank" rel="noopener">https://www.gso.gov.vn/en/</a></div>
|
565 |
+
<div class="ref" id="ref-5"><strong>5.</strong> Wikipedia - Economy of Vietnam — <a href="https://en.wikipedia.org/wiki/Economy_of_Vietnam" target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Economy_of_Vietnam</a></div>
|
566 |
+
<div class="ref" id="ref-6"><strong>6.</strong> IMF - Vietnam and the IMF — <a href="https://www.imf.org/en/Countries/VNM" target="_blank" rel="noopener">https://www.imf.org/en/Countries/VNM</a></div>
|
567 |
+
<div class="ref" id="ref-7"><strong>7.</strong> FocusEconomics - Vietnam Economic Indicators — <a href="https://www.focus-economics.com/countries/vietnam" target="_blank" rel="noopener">https://www.focus-economics.com/countries/vietnam</a></div>
|
568 |
+
<div class="ref" id="ref-8"><strong>8.</strong> National Statistics Office of Vietnam - Economic Reports — <a href="https://www.gso.gov.vn/en/data-and-statistics/" target="_blank" rel="noopener">https://www.gso.gov.vn/en/data-and-statistics/</a></div>
|
569 |
+
<div class="ref" id="ref-9"><strong>9.</strong> VietnamNet - Economic News and Analysis — <a href="https://vietnamnet.vn/" target="_blank" rel="noopener">https://vietnamnet.vn/</a></div>
|
570 |
+
<div class="ref" id="ref-10"><strong>10.</strong> IMF - Article IV Mission Reports — <a href="https://www.imf.org/en/Publications/CR" target="_blank" rel="noopener">https://www.imf.org/en/Publications/CR</a></div>
|
571 |
+
<div class="ref" id="ref-11"><strong>11.</strong> Vietnam Briefing - Economic Analysis — <a href="https://www.vietnam-briefing.com/" target="_blank" rel="noopener">https://www.vietnam-briefing.com/</a></div>
|
572 |
+
<div class="ref" id="ref-12"><strong>12.</strong> Vietnam Investment Review - FDI Statistics — <a href="https://vir.com.vn/" target="_blank" rel="noopener">https://vir.com.vn/</a></div>
|
573 |
+
<div class="ref" id="ref-13"><strong>13.</strong> Trading Economics - Vietnam Foreign Direct Investment — <a href="https://tradingeconomics.com/vietnam/foreign-direct-investment" target="_blank" rel="noopener">https://tradingeconomics.com/vietnam/foreign-direct-investment</a></div>
|
574 |
+
<div class="ref" id="ref-14"><strong>14.</strong> White & Case - Regional Economic Outlook — <a href="https://www.whitecase.com/" target="_blank" rel="noopener">https://www.whitecase.com/</a></div>
|
575 |
+
<div class="ref" id="ref-15"><strong>15.</strong> Vietnam Economic Times — <a href="https://vneconomictimes.com/" target="_blank" rel="noopener">https://vneconomictimes.com/</a></div>
|
576 |
+
<div class="ref" id="ref-16"><strong>16.</strong> Asian Development Bank - Vietnam Country Partnership — <a href="https://www.adb.org/countries/viet-nam/main" target="_blank" rel="noopener">https://www.adb.org/countries/viet-nam/main</a></div>
|
577 |
+
<div class="ref" id="ref-17"><strong>17.</strong> Ministry of Planning and Investment - Vietnam — <a href="https://www.mpi.gov.vn/en/" target="_blank" rel="noopener">https://www.mpi.gov.vn/en/</a></div>
|
578 |
+
</section>
|
579 |
+
|
580 |
+
<section id="appendix" data-title="Appendices">
|
581 |
+
<h2 class="section-title">Appendices <button class="icon ghost small copy-link" aria-label="Copy link to section"><i class="ph ph-link"></i></button></h2>
|
582 |
+
<details open>
|
583 |
+
<summary><i class="ph ph-paperclip"></i> Appendix A — Definitions</summary>
|
584 |
+
<ul>
|
585 |
+
<li>GDP growth: Year-on-year percentage change at constant prices.</li>
|
586 |
+
<li>FDI registered vs disbursed: Approved capital vs realized inflows.</li>
|
587 |
+
<li>Inflation: CPI, YoY % change.</li>
|
588 |
+
<li>Unemployment rate: National measure, % of labor force.</li>
|
589 |
+
</ul>
|
590 |
+
</details>
|
591 |
+
<details>
|
592 |
+
<summary><i class="ph ph-paperclip"></i> Appendix B — Chart Methods</summary>
|
593 |
+
<ul>
|
594 |
+
<li>Bar and line charts use linear scales with zero baselines for bars and min–max padding for lines.</li>
|
595 |
+
<li>Donut chart proportions reflect category share of total.</li>
|
596 |
+
<li>Heatmap scales each indicator column to its min–max domain with perceptually uniform color scale.</li>
|
597 |
+
</ul>
|
598 |
+
</details>
|
599 |
+
</section>
|
600 |
+
|
601 |
+
</main>
|
602 |
+
</div>
|
603 |
+
</div>
|
604 |
+
|
605 |
+
<div class="toc-drawer" id="tocDrawer" aria-hidden="true">
|
606 |
+
<div class="toc-panel">
|
607 |
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px">
|
608 |
+
<strong>Table of Contents</strong>
|
609 |
+
<button class="icon ghost" id="closeTOC" aria-label="Close table of contents"><i class="ph ph-x"></i></button>
|
610 |
+
</div>
|
611 |
+
<nav>
|
612 |
+
<a href="#exec" class="toc-link"><i class="ph ph-sparkle"></i> Executive Summary</a>
|
613 |
+
<a href="#methodology" class="toc-link"><i class="ph ph-flask"></i> Methodology</a>
|
614 |
+
<a href="#indicators" class="toc-link"><i class="ph ph-gauge"></i> Key Indicators</a>
|
615 |
+
<a href="#sector" class="toc-link"><i class="ph ph-buildings"></i> Sectoral Analysis</a>
|
616 |
+
<a href="#challenges" class="toc-link"><i class="ph ph-warning"></i> Challenges & Risks</a>
|
617 |
+
<a href="#history" class="toc-link"><i class="ph ph-timer"></i> Historical Comparison</a>
|
618 |
+
<a href="#outlook" class="toc-link"><i class="ph ph-compass"></i> Outlook & Projections</a>
|
619 |
+
<a href="#data" class="toc-link"><i class="ph ph-table"></i> Data Explorer</a>
|
620 |
+
<a href="#conclusion" class="toc-link"><i class="ph ph-check-circle"></i> Conclusion</a>
|
621 |
+
<a href="#references" class="toc-link"><i class="ph ph-bookmarks"></i> Sources & Citations</a>
|
622 |
+
<a href="#appendix" class="toc-link"><i class="ph ph-note"></i> Appendices</a>
|
623 |
+
</nav>
|
624 |
+
</div>
|
625 |
+
</div>
|
626 |
+
|
627 |
+
<div class="sticky-banner hidden" id="searchBanner"><i class="ph ph-magnifying-glass"></i><span id="searchCount">0 results</span><button class="btn small" id="clearSearch"><i class="ph ph-x-circle"></i> Clear</button></div>
|
628 |
+
|
629 |
+
<footer>
|
630 |
+
Built with modern web standards: CSS Grid, Flexbox, Container Queries, clamp(), aspect-ratio, and vanilla JavaScript (IntersectionObserver, LocalStorage, Clipboard, Canvas).
|
631 |
+
</footer>
|
632 |
+
|
633 |
+
<script>
|
634 |
+
/* Theme and preferences */
|
635 |
+
const prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
636 |
+
const savedTheme = localStorage.getItem('theme');
|
637 |
+
if(savedTheme === 'light' || (!savedTheme && prefersLight)){ document.documentElement.classList.add('light'); }
|
638 |
+
const themeToggle = document.getElementById('themeToggle');
|
639 |
+
themeToggle.addEventListener('click', ()=>{
|
640 |
+
document.documentElement.classList.toggle('light');
|
641 |
+
localStorage.setItem('theme', document.documentElement.classList.contains('light') ? 'light' : 'dark');
|
642 |
+
themeToggle.innerHTML = document.documentElement.classList.contains('light') ? '<i class="ph ph-sun"></i>' : '<i class="ph ph-moon-stars"></i>';
|
643 |
+
});
|
644 |
+
themeToggle.innerHTML = document.documentElement.classList.contains('light') ? '<i class="ph ph-sun"></i>' : '<i class="ph ph-moon-stars"></i>';
|
645 |
+
|
646 |
+
/* Smooth anchor highlight and TOC spy */
|
647 |
+
const tocLinks = Array.from(document.querySelectorAll('.toc a, .toc-panel a'));
|
648 |
+
const sections = Array.from(document.querySelectorAll('main section'));
|
649 |
+
const observer = new IntersectionObserver((entries)=>{
|
650 |
+
entries.forEach(entry=>{
|
651 |
+
if(entry.isIntersecting){
|
652 |
+
const id = entry.target.id;
|
653 |
+
tocLinks.forEach(a=>{
|
654 |
+
a.classList.toggle('active', a.getAttribute('href') === '#'+id);
|
655 |
+
});
|
656 |
+
localStorage.setItem('lastSection', id);
|
657 |
+
}
|
658 |
+
});
|
659 |
+
}, { rootMargin: '0px 0px -60% 0px', threshold: 0.2 });
|
660 |
+
sections.forEach(s=>observer.observe(s));
|
661 |
+
|
662 |
+
/* Progress bar */
|
663 |
+
const progress = document.getElementById('progress');
|
664 |
+
document.addEventListener('scroll', ()=>{
|
665 |
+
const scrolled = window.scrollY;
|
666 |
+
const max = document.documentElement.scrollHeight - window.innerHeight;
|
667 |
+
const ratio = Math.max(0, Math.min(1, scrolled / max));
|
668 |
+
progress.style.transform = `scaleX(${ratio})`;
|
669 |
+
});
|
670 |
+
|
671 |
+
/* TOC mobile drawer */
|
672 |
+
const drawer = document.getElementById('tocDrawer');
|
673 |
+
document.getElementById('openTOC').addEventListener('click', ()=>{ drawer.style.display='block'; drawer.setAttribute('aria-hidden','false'); });
|
674 |
+
document.getElementById('closeTOC').addEventListener('click', ()=>{ drawer.style.display='none'; drawer.setAttribute('aria-hidden','true'); });
|
675 |
+
drawer.addEventListener('click', (e)=>{ if(e.target===drawer){ drawer.style.display='none'; drawer.setAttribute('aria-hidden','true'); }});
|
676 |
+
tocLinks.forEach(a=>a.addEventListener('click', ()=>{ drawer.style.display='none'; drawer.setAttribute('aria-hidden','true'); }));
|
677 |
+
|
678 |
+
/* Copy section link buttons */
|
679 |
+
document.querySelectorAll('.copy-link').forEach(btn=>{
|
680 |
+
btn.addEventListener('click', async (e)=>{
|
681 |
+
const section = e.target.closest('section');
|
682 |
+
const url = `${location.origin}${location.pathname}#${section.id}`;
|
683 |
+
try{
|
684 |
+
await navigator.clipboard.writeText(url);
|
685 |
+
toast('Section link copied');
|
686 |
+
history.replaceState(null,'','#'+section.id);
|
687 |
+
}catch(err){ console.error(err); }
|
688 |
+
});
|
689 |
+
});
|
690 |
+
|
691 |
+
/* Global share & print */
|
692 |
+
document.getElementById('shareReport').addEventListener('click', async ()=>{
|
693 |
+
const url = location.href;
|
694 |
+
if(navigator.share){
|
695 |
+
await navigator.share({ title: document.title, url });
|
696 |
+
}else{
|
697 |
+
await navigator.clipboard.writeText(url);
|
698 |
+
toast('Report link copied');
|
699 |
+
}
|
700 |
+
});
|
701 |
+
document.getElementById('printReport').addEventListener('click', ()=>window.print());
|
702 |
+
|
703 |
+
/* Keyboard shortcut to focus search */
|
704 |
+
const searchInput = document.getElementById('searchInput');
|
705 |
+
window.addEventListener('keydown', (e)=>{
|
706 |
+
if(e.key === '/' && document.activeElement !== searchInput){
|
707 |
+
e.preventDefault();
|
708 |
+
searchInput.focus();
|
709 |
+
}
|
710 |
+
});
|
711 |
+
|
712 |
+
/* Search implementation with highlight */
|
713 |
+
const searchBanner = document.getElementById('searchBanner');
|
714 |
+
const searchCount = document.getElementById('searchCount');
|
715 |
+
const clearSearch = document.getElementById('clearSearch');
|
716 |
+
let lastHighlights = [];
|
717 |
+
function clearHighlights(){
|
718 |
+
lastHighlights.forEach(el=>{
|
719 |
+
const parent = el.parentNode;
|
720 |
+
parent.replaceChild(document.createTextNode(el.textContent), el);
|
721 |
+
parent.normalize();
|
722 |
+
});
|
723 |
+
lastHighlights = [];
|
724 |
+
}
|
725 |
+
function highlightText(node, query){
|
726 |
+
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
|
727 |
+
acceptNode: (n)=> n.nodeValue.trim().length && !n.parentElement.closest('script,style,header,nav,footer,button,svg,canvas')
|
728 |
+
});
|
729 |
+
let count=0;
|
730 |
+
while(walker.nextNode()){
|
731 |
+
const txt = walker.currentNode.nodeValue;
|
732 |
+
const idx = txt.toLowerCase().indexOf(query.toLowerCase());
|
733 |
+
if(idx>-1){
|
734 |
+
const mark = document.createElement('span');
|
735 |
+
mark.className='highlight';
|
736 |
+
mark.textContent = txt.substr(idx, query.length);
|
737 |
+
const after = document.createTextNode(txt.substr(idx+query.length));
|
738 |
+
const before = document.createTextNode(txt.substr(0, idx));
|
739 |
+
const parent = walker.currentNode.parentNode;
|
740 |
+
parent.replaceChild(after, walker.currentNode);
|
741 |
+
parent.insertBefore(mark, after);
|
742 |
+
parent.insertBefore(before, mark);
|
743 |
+
lastHighlights.push(mark);
|
744 |
+
count++;
|
745 |
+
}
|
746 |
+
}
|
747 |
+
return count;
|
748 |
+
}
|
749 |
+
searchInput.addEventListener('input', ()=>{
|
750 |
+
clearHighlights();
|
751 |
+
const q = searchInput.value.trim();
|
752 |
+
if(!q){
|
753 |
+
searchBanner.classList.add('hidden');
|
754 |
+
return;
|
755 |
+
}
|
756 |
+
let total=0;
|
757 |
+
sections.forEach(sec => total += highlightText(sec, q));
|
758 |
+
searchBanner.classList.remove('hidden');
|
759 |
+
searchCount.textContent = `${total} result${total===1?'':'s'}`;
|
760 |
+
localStorage.setItem('searchQuery', q);
|
761 |
+
});
|
762 |
+
clearSearch.addEventListener('click', ()=>{
|
763 |
+
searchInput.value='';
|
764 |
+
clearHighlights();
|
765 |
+
searchBanner.classList.add('hidden');
|
766 |
+
localStorage.removeItem('searchQuery');
|
767 |
+
});
|
768 |
+
/* Restore last search */
|
769 |
+
const savedQuery = localStorage.getItem('searchQuery');
|
770 |
+
if(savedQuery){ searchInput.value = savedQuery; searchInput.dispatchEvent(new Event('input')); }
|
771 |
+
|
772 |
+
/* Data for visualizations */
|
773 |
+
const data = {
|
774 |
+
gdp2025: [
|
775 |
+
{ label: 'Q1 2025 (actual)', value: 6.9, type: 'Actual', src: 4 },
|
776 |
+
{ label: 'Q2 2025 (actual)', value: 7.96, type: 'Actual', src: 1 },
|
777 |
+
{ label: 'H1 2025 (actual)', value: 7.52, type: 'Actual', src: 4 },
|
778 |
+
{ label: 'World Bank (forecast)', value: 5.8, type: 'Forecast', src: 2 },
|
779 |
+
{ label: 'ADB (forecast)', value: 6.6, type: 'Forecast', src: 16 },
|
780 |
+
{ label: 'IMF (forecast)', value: 5.2, type: 'Forecast', src: 2 },
|
781 |
+
{ label: 'Gov target (midpoint)', value: 8.4, type: 'Target', src: 2 }, // midpoint of 8.3–8.5
|
782 |
+
],
|
783 |
+
q1Series: {
|
784 |
+
labels: ['2020','2021','2022','2023','2024','2025'],
|
785 |
+
values: [3.21,4.85,5.42,3.46,5.98,6.93],
|
786 |
+
src: 1
|
787 |
+
},
|
788 |
+
fdi: [
|
789 |
+
{ label:'Registered', value: 18.4, color: '#4cc9f0', unit:'US$ bn', src:12 },
|
790 |
+
{ label:'Disbursed', value: 8.9, color: '#72efdd', unit:'US$ bn', src:13 },
|
791 |
+
],
|
792 |
+
heatmap: {
|
793 |
+
rows: ['IMF','ADB','World Bank','Government'],
|
794 |
+
cols: ['GDP growth (%)','Inflation (%)'],
|
795 |
+
values: [
|
796 |
+
[5.2, 2.9], // IMF
|
797 |
+
[6.6, 4.0], // ADB
|
798 |
+
[5.8, null], // WB (no inflation in report)
|
799 |
+
[8.4, null] // Government target midpoint for GDP
|
800 |
+
],
|
801 |
+
srcs: [
|
802 |
+
[2,2],
|
803 |
+
[16,16],
|
804 |
+
[2,null],
|
805 |
+
[2,null]
|
806 |
+
]
|
807 |
+
}
|
808 |
+
};
|
809 |
+
|
810 |
+
/* Minimal charting utilities (Canvas) */
|
811 |
+
function createCanvasContext(container){
|
812 |
+
const canvas = container.querySelector('canvas');
|
813 |
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
814 |
+
const rect = canvas.getBoundingClientRect();
|
815 |
+
canvas.width = Math.floor(rect.width * dpr);
|
816 |
+
canvas.height = Math.floor(rect.height * dpr);
|
817 |
+
const ctx = canvas.getContext('2d');
|
818 |
+
ctx.scale(dpr, dpr);
|
819 |
+
return { canvas, ctx, dpr };
|
820 |
+
}
|
821 |
+
function formatPct(v){ return `${(Math.round(v*100)/100).toFixed(2)}%`; }
|
822 |
+
function drawAxes(ctx, {x,y,w,h}, xTicks, yTicks){
|
823 |
+
ctx.save();
|
824 |
+
ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--border');
|
825 |
+
ctx.lineWidth = 1;
|
826 |
+
// axes
|
827 |
+
ctx.beginPath();
|
828 |
+
ctx.moveTo(x, y);
|
829 |
+
ctx.lineTo(x, y+h);
|
830 |
+
ctx.lineTo(x+w, y+h);
|
831 |
+
ctx.stroke();
|
832 |
+
// grid + labels
|
833 |
+
ctx.font = '12px Inter';
|
834 |
+
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--muted');
|
835 |
+
ctx.textBaseline = 'middle';
|
836 |
+
const gh = h / (yTicks.values.length-1);
|
837 |
+
yTicks.values.forEach((v, i)=>{
|
838 |
+
const yy = y+h - i*gh;
|
839 |
+
ctx.globalAlpha=.5;
|
840 |
+
ctx.beginPath(); ctx.moveTo(x, yy); ctx.lineTo(x+w, yy); ctx.stroke();
|
841 |
+
ctx.globalAlpha=1;
|
842 |
+
ctx.fillText(v.toFixed(0)+'%', x-6, yy);
|
843 |
+
});
|
844 |
+
ctx.textBaseline='top';
|
845 |
+
const gw = w / xTicks.length;
|
846 |
+
xTicks.forEach((t, i)=>{
|
847 |
+
const xx = x + i*gw + gw/2;
|
848 |
+
ctx.save();
|
849 |
+
ctx.translate(xx, y+h+6);
|
850 |
+
ctx.rotate(-Math.PI/8);
|
851 |
+
ctx.fillText(t, 0, 0);
|
852 |
+
ctx.restore();
|
853 |
+
});
|
854 |
+
ctx.restore();
|
855 |
+
}
|
856 |
+
function colorVars(){ const cs = getComputedStyle(document.documentElement); return {
|
857 |
+
brand: cs.getPropertyValue('--brand').trim(),
|
858 |
+
brand2: cs.getPropertyValue('--brand-2').trim(),
|
859 |
+
accent: cs.getPropertyValue('--accent').trim(),
|
860 |
+
border: cs.getPropertyValue('--border').trim(),
|
861 |
+
text: cs.getPropertyValue('--text').trim()
|
862 |
+
};}
|
863 |
+
|
864 |
+
/* Bar chart */
|
865 |
+
function renderBar(container, series){
|
866 |
+
const {canvas, ctx} = createCanvasContext(container);
|
867 |
+
const padding = {l: 44, r: 12, t: 16, b: 54};
|
868 |
+
const plot = {x: padding.l, y: padding.t, w: canvas.width/(window.devicePixelRatio||1) - padding.l - padding.r, h: canvas.height/(window.devicePixelRatio||1) - padding.t - padding.b};
|
869 |
+
const max = Math.max(...series.map(d=>d.value))*1.15;
|
870 |
+
const yTicks = { values: Array.from({length:6},(_,i)=> i*(max/5)) };
|
871 |
+
const xTicks = series.map(d=>d.label);
|
872 |
+
drawAxes(ctx, plot, xTicks, yTicks);
|
873 |
+
|
874 |
+
const cols = { Actual: colorVars().brand, Forecast: colorVars().accent, Target: '#22c55e' };
|
875 |
+
const barW = plot.w / series.length * 0.7;
|
876 |
+
const gap = plot.w / series.length - barW;
|
877 |
+
ctx.save();
|
878 |
+
ctx.textAlign='center'; ctx.textBaseline='bottom'; ctx.fillStyle = colorVars().text;
|
879 |
+
series.forEach((d, i)=>{
|
880 |
+
const x0 = plot.x + i*(barW+gap) + gap/2;
|
881 |
+
const y0 = plot.y + plot.h;
|
882 |
+
const h = (d.value/max) * plot.h;
|
883 |
+
ctx.fillStyle = cols[d.type] || colorVars().brand2;
|
884 |
+
ctx.beginPath(); ctx.roundRect(x0, y0 - h, barW, h, 6); ctx.fill();
|
885 |
+
});
|
886 |
+
ctx.restore();
|
887 |
+
|
888 |
+
// Legend
|
889 |
+
const legend = container.querySelector('.legend');
|
890 |
+
legend.innerHTML = '';
|
891 |
+
Object.entries({Actual: colorVars().brand, Forecast: colorVars().accent, Target: '#22c55e'}).forEach(([k,c])=>{
|
892 |
+
const item = document.createElement('span');
|
893 |
+
item.className='key';
|
894 |
+
item.innerHTML = `<span class="swatch" style="background:${c}"></span>${k}`;
|
895 |
+
legend.appendChild(item);
|
896 |
+
});
|
897 |
+
|
898 |
+
// Tooltip
|
899 |
+
const tooltip = container.querySelector('.tooltip');
|
900 |
+
canvas.addEventListener('mousemove', (e)=>{
|
901 |
+
const rect = canvas.getBoundingClientRect();
|
902 |
+
const x = (e.clientX - rect.left);
|
903 |
+
const y = (e.clientY - rect.top);
|
904 |
+
const scale = window.devicePixelRatio || 1;
|
905 |
+
const px = x, py = y;
|
906 |
+
const barWpx = barW;
|
907 |
+
const gapPx = plot.w / series.length - barWpx;
|
908 |
+
let hit = null;
|
909 |
+
series.forEach((d, i)=>{
|
910 |
+
const bx = plot.x + i*(barWpx+gapPx) + gapPx/2;
|
911 |
+
const bh = (d.value/max) * plot.h;
|
912 |
+
const by = plot.y + plot.h - bh;
|
913 |
+
if(px>=bx && px<=bx+barWpx && py>=by && py<=by+bh) hit = {d, bx, by, bh};
|
914 |
+
});
|
915 |
+
if(hit){
|
916 |
+
tooltip.style.display='block';
|
917 |
+
tooltip.style.left = (hit.bx + barW/2)+'px';
|
918 |
+
tooltip.style.top = (hit.by)+'px';
|
919 |
+
tooltip.innerHTML = `<strong>${hit.d.label}</strong><br>${formatPct(hit.d.value)} (${hit.d.type}) [<a href="#ref-${hit.d.src}">${hit.d.src}</a>]`;
|
920 |
+
}else{
|
921 |
+
tooltip.style.display='none';
|
922 |
+
}
|
923 |
+
});
|
924 |
+
container.querySelector('.export-png').onclick = ()=> exportCanvasPNG(canvas, 'vietnam-gdp-2025.png');
|
925 |
+
container.querySelector('.copy-data').onclick = ()=> copyJSON(series);
|
926 |
+
}
|
927 |
+
|
928 |
+
/* Line chart */
|
929 |
+
function renderLine(container, series){
|
930 |
+
const {canvas, ctx} = createCanvasContext(container);
|
931 |
+
const padding = {l: 44, r: 16, t: 16, b: 40};
|
932 |
+
const plot = {x: padding.l, y: padding.t, w: canvas.width/(window.devicePixelRatio||1) - padding.l - padding.r, h: canvas.height/(window.devicePixelRatio||1) - padding.t - padding.b};
|
933 |
+
const min = Math.min(...series.values)*0.9; const max = Math.max(...series.values)*1.1;
|
934 |
+
const yTicks = { values: Array.from({length:6},(_,i)=> min + i*(max-min)/5) };
|
935 |
+
const xTicks = series.labels;
|
936 |
+
drawAxes(ctx, plot, xTicks, yTicks);
|
937 |
+
|
938 |
+
// line
|
939 |
+
ctx.save();
|
940 |
+
ctx.strokeStyle = colorVars().brand;
|
941 |
+
ctx.lineWidth = 2;
|
942 |
+
ctx.beginPath();
|
943 |
+
series.values.forEach((v,i)=>{
|
944 |
+
const x = plot.x + (i/(series.values.length-1))*plot.w;
|
945 |
+
const y = plot.y + plot.h - ((v-min)/(max-min))*plot.h;
|
946 |
+
i===0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
|
947 |
+
});
|
948 |
+
ctx.stroke();
|
949 |
+
// points
|
950 |
+
ctx.fillStyle = colorVars().brand2;
|
951 |
+
series.values.forEach((v,i)=>{
|
952 |
+
const x = plot.x + (i/(series.values.length-1))*plot.w;
|
953 |
+
const y = plot.y + plot.h - ((v-min)/(max-min))*plot.h;
|
954 |
+
ctx.beginPath(); ctx.arc(x,y,4,0,Math.PI*2); ctx.fill();
|
955 |
+
});
|
956 |
+
ctx.restore();
|
957 |
+
|
958 |
+
// Legend
|
959 |
+
const legend = container.querySelector('.legend');
|
960 |
+
legend.innerHTML = `<span class="key"><span class="swatch" style="background:${colorVars().brand}"></span>Q1 YoY GDP</span>`;
|
961 |
+
|
962 |
+
const tooltip = container.querySelector('.tooltip');
|
963 |
+
const pts = series.values.map((v,i)=>{
|
964 |
+
const x = plot.x + (i/(series.values.length-1))*plot.w;
|
965 |
+
const y = plot.y + plot.h - ((v-min)/(max-min))*plot.h;
|
966 |
+
return {x,y,label:series.labels[i],v,src:series.src};
|
967 |
+
});
|
968 |
+
canvas.addEventListener('mousemove',(e)=>{
|
969 |
+
const rect = canvas.getBoundingClientRect();
|
970 |
+
const x = (e.clientX - rect.left);
|
971 |
+
const y = (e.clientY - rect.top);
|
972 |
+
let hit=null, dmin=12;
|
973 |
+
pts.forEach(p=>{
|
974 |
+
const d = Math.hypot(p.x-x,p.y-y);
|
975 |
+
if(d<dmin){ dmin=d; hit=p; }
|
976 |
+
});
|
977 |
+
if(hit){
|
978 |
+
tooltip.style.display='block';
|
979 |
+
tooltip.style.left = (hit.x)+'px';
|
980 |
+
tooltip.style.top = (hit.y)+'px';
|
981 |
+
tooltip.innerHTML = `<strong>${hit.label}</strong><br>${formatPct(hit.v)} [<a href="#ref-${hit.src}">${hit.src}</a>]`;
|
982 |
+
}else{
|
983 |
+
tooltip.style.display='none';
|
984 |
+
}
|
985 |
+
});
|
986 |
+
container.querySelector('.export-png').onclick = ()=> exportCanvasPNG(canvas, 'vietnam-q1-2020-2025.png');
|
987 |
+
container.querySelector('.copy-data').onclick = ()=> copyJSON(series);
|
988 |
+
}
|
989 |
+
|
990 |
+
/* Donut chart */
|
991 |
+
function renderDonut(container, series){
|
992 |
+
const {canvas, ctx} = createCanvasContext(container);
|
993 |
+
const rectW = canvas.width/(window.devicePixelRatio||1);
|
994 |
+
const rectH = canvas.height/(window.devicePixelRatio||1);
|
995 |
+
const cx = rectW/2, cy = rectH/2, r = Math.min(rectW, rectH)*0.34;
|
996 |
+
const total = series.reduce((s,d)=>s+d.value,0);
|
997 |
+
let start = -Math.PI/2;
|
998 |
+
series.forEach(d=>{
|
999 |
+
const angle = (d.value/total)*Math.PI*2;
|
1000 |
+
ctx.beginPath();
|
1001 |
+
ctx.fillStyle = d.color || colorVars().brand;
|
1002 |
+
ctx.moveTo(cx,cy);
|
1003 |
+
ctx.arc(cx,cy,r,start,start+angle);
|
1004 |
+
ctx.closePath(); ctx.fill();
|
1005 |
+
start += angle;
|
1006 |
+
});
|
1007 |
+
// hole
|
1008 |
+
ctx.globalCompositeOperation='destination-out';
|
1009 |
+
ctx.beginPath(); ctx.arc(cx,cy,r*0.6,0,Math.PI*2); ctx.fill();
|
1010 |
+
ctx.globalCompositeOperation='source-over';
|
1011 |
+
// labels
|
1012 |
+
ctx.fillStyle = colorVars().text; ctx.textAlign='center';
|
1013 |
+
ctx.font='700 18px Inter';
|
1014 |
+
ctx.fillText('FDI', cx, cy-4);
|
1015 |
+
ctx.font='12px Inter'; ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--muted');
|
1016 |
+
ctx.fillText(`Total ${total.toFixed(1)} US$ bn`, cx, cy+12);
|
1017 |
+
|
1018 |
+
const legend = container.querySelector('.legend'); legend.innerHTML='';
|
1019 |
+
series.forEach(s=>{
|
1020 |
+
const item = document.createElement('span');
|
1021 |
+
item.className='key';
|
1022 |
+
item.innerHTML = `<span class="swatch" style="background:${s.color}"></span>${s.label}`;
|
1023 |
+
legend.appendChild(item);
|
1024 |
+
});
|
1025 |
+
|
1026 |
+
// tooltip
|
1027 |
+
const tooltip = container.querySelector('.tooltip');
|
1028 |
+
canvas.addEventListener('mousemove',(e)=>{
|
1029 |
+
const rect = canvas.getBoundingClientRect();
|
1030 |
+
const x = e.clientX - rect.left - cx;
|
1031 |
+
const y = e.clientY - rect.top - cy;
|
1032 |
+
const dist = Math.hypot(x,y);
|
1033 |
+
if(dist < r && dist > r*0.6){
|
1034 |
+
let angle = Math.atan2(y,x); if(angle < -Math.PI/2) angle += Math.PI*2;
|
1035 |
+
let acc = -Math.PI/2;
|
1036 |
+
let found=null;
|
1037 |
+
series.forEach(s=>{
|
1038 |
+
const a = (s.value/total)*Math.PI*2;
|
1039 |
+
if(angle>=acc && angle<=acc+a) found=s;
|
1040 |
+
acc+=a;
|
1041 |
+
});
|
1042 |
+
if(found){
|
1043 |
+
const pct = (found.value/total)*100;
|
1044 |
+
tooltip.style.display='block';
|
1045 |
+
tooltip.style.left = (e.clientX - rect.left)+'px';
|
1046 |
+
tooltip.style.top = (e.clientY - rect.top - 12)+'px';
|
1047 |
+
tooltip.innerHTML = `<strong>${found.label}</strong><br>${found.value} ${found.unit} (${pct.toFixed(1)}%) [<a href="#ref-${found.src}">${found.src}</a>]`;
|
1048 |
+
}
|
1049 |
+
}else{
|
1050 |
+
tooltip.style.display='none';
|
1051 |
+
}
|
1052 |
+
});
|
1053 |
+
container.querySelector('.export-png').onclick = ()=> exportCanvasPNG(canvas, 'vietnam-fdi-2025.png');
|
1054 |
+
container.querySelector('.copy-data').onclick = ()=> copyJSON(series);
|
1055 |
+
}
|
1056 |
+
|
1057 |
+
/* Heatmap */
|
1058 |
+
function renderHeatmap(container, mat){
|
1059 |
+
const {canvas, ctx} = createCanvasContext(container);
|
1060 |
+
const rectW = canvas.width/(window.devicePixelRatio||1);
|
1061 |
+
const rectH = canvas.height/(window.devicePixelRatio||1);
|
1062 |
+
const padding = {l: 140, r: 16, t: 28, b: 44};
|
1063 |
+
const rows = mat.rows.length, cols = mat.cols.length;
|
1064 |
+
const cellW = (rectW - padding.l - padding.r) / cols;
|
1065 |
+
const cellH = (rectH - padding.t - padding.b) / rows;
|
1066 |
+
|
1067 |
+
// Color scale
|
1068 |
+
const valsGDP = mat.values.map(r=>r[0]).filter(v=>v!=null);
|
1069 |
+
const minGDP = Math.min(...valsGDP), maxGDP = Math.max(...valsGDP);
|
1070 |
+
const valsInf = mat.values.map(r=>r[1]).filter(v=>v!=null);
|
1071 |
+
const minInf = Math.min(...valsInf), maxInf = Math.max(...valsInf);
|
1072 |
+
function lerp(a,b,t){ return a + (b-a)*t; }
|
1073 |
+
function colorScale(v, min, max){
|
1074 |
+
const t = (v-min)/(max-min || 1); // 0..1
|
1075 |
+
const c1 = hexToRgb(colorVars().brand); // low
|
1076 |
+
const c2 = hexToRgb('#22c55e'); // high
|
1077 |
+
const r = Math.round(lerp(c1.r,c2.r,t));
|
1078 |
+
const g = Math.round(lerp(c1.g,c2.g,t));
|
1079 |
+
const b = Math.round(lerp(c1.b,c2.b,t));
|
1080 |
+
return `rgb(${r},${g},${b})`;
|
1081 |
+
}
|
1082 |
+
function hexToRgb(hex){ const m = hex.replace('#',''); return { r: parseInt(m.substring(0,2),16), g: parseInt(m.substring(2,4),16), b: parseInt(m.substring(4,6),16)}; }
|
1083 |
+
|
1084 |
+
ctx.font='12px Inter';
|
1085 |
+
ctx.textAlign='center';
|
1086 |
+
ctx.textBaseline='middle';
|
1087 |
+
|
1088 |
+
// Draw cells
|
1089 |
+
for(let r=0;r<rows;r++){
|
1090 |
+
for(let c=0;c<cols;c++){
|
1091 |
+
const v = mat.values[r][c];
|
1092 |
+
const x = padding.l + c*cellW;
|
1093 |
+
const y = padding.t + r*cellH;
|
1094 |
+
if(v==null){
|
1095 |
+
ctx.fillStyle = 'transparent';
|
1096 |
+
ctx.fillRect(x,y,cellW,cellH);
|
1097 |
+
// hatch
|
1098 |
+
ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--border');
|
1099 |
+
ctx.beginPath();
|
1100 |
+
ctx.moveTo(x+4, y+4); ctx.lineTo(x+cellW-4, y+cellH-4);
|
1101 |
+
ctx.moveTo(x+cellW-4, y+4); ctx.lineTo(x+4, y+cellH-4);
|
1102 |
+
ctx.stroke();
|
1103 |
+
ctx.strokeRect(x,y,cellW,cellH);
|
1104 |
+
continue;
|
1105 |
+
}
|
1106 |
+
const min = c===0 ? minGDP : minInf;
|
1107 |
+
const max = c===0 ? maxGDP : maxInf;
|
1108 |
+
ctx.fillStyle = colorScale(v, min, max);
|
1109 |
+
ctx.fillRect(x,y,cellW,cellH);
|
1110 |
+
ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--border');
|
1111 |
+
ctx.strokeRect(x,y,cellW,cellH);
|
1112 |
+
ctx.fillStyle = '#0b1a26';
|
1113 |
+
const themeIsLight = document.documentElement.classList.contains('light');
|
1114 |
+
ctx.fillStyle = themeIsLight ? '#0b1a26' : '#00131f';
|
1115 |
+
ctx.globalAlpha=.6; ctx.fillRect(x+6, y+6, cellW-12, 18); ctx.globalAlpha=1;
|
1116 |
+
ctx.fillStyle = themeIsLight ? '#0b1a26' : '#cde9f8';
|
1117 |
+
ctx.fillText(v.toFixed(1), x+cellW/2, y+cellH/2);
|
1118 |
+
}
|
1119 |
+
}
|
1120 |
+
// Row labels
|
1121 |
+
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--text');
|
1122 |
+
ctx.textAlign='right'; ctx.textBaseline='middle';
|
1123 |
+
for(let r=0;r<rows;r++){
|
1124 |
+
const y = padding.t + r*cellH + cellH/2;
|
1125 |
+
ctx.fillText(mat.rows[r], padding.l - 10, y);
|
1126 |
+
}
|
1127 |
+
// Col labels
|
1128 |
+
ctx.textAlign='center'; ctx.textBaseline='bottom';
|
1129 |
+
for(let c=0;c<cols;c++){
|
1130 |
+
const x = padding.l + c*cellW + cellW/2;
|
1131 |
+
ctx.fillText(mat.cols[c], x, padding.t - 6);
|
1132 |
+
}
|
1133 |
+
// Tooltip
|
1134 |
+
const tooltip = container.querySelector('.tooltip');
|
1135 |
+
canvas.addEventListener('mousemove',(e)=>{
|
1136 |
+
const rect = canvas.getBoundingClientRect();
|
1137 |
+
const mx = e.clientX - rect.left;
|
1138 |
+
const my = e.clientY - rect.top;
|
1139 |
+
const cx = Math.floor((mx - padding.l)/cellW);
|
1140 |
+
const cy = Math.floor((my - padding.t)/cellH);
|
1141 |
+
if(cx>=0 && cx<cols && cy>=0 && cy<rows){
|
1142 |
+
const v = mat.values[cy][cx];
|
1143 |
+
const src = mat.srcs[cy][cx];
|
1144 |
+
tooltip.style.display='block';
|
1145 |
+
tooltip.style.left = (mx)+'px';
|
1146 |
+
tooltip.style.top = (my-10)+'px';
|
1147 |
+
if(v==null){
|
1148 |
+
tooltip.innerHTML = `<strong>${mat.rows[cy]}</strong> — ${mat.cols[cx]}<br><em>n/a in provided report</em>`;
|
1149 |
+
}else{
|
1150 |
+
tooltip.innerHTML = `<strong>${mat.rows[cy]}</strong> — ${mat.cols[cx]}<br>${v.toFixed(1)}%` + (src?` [<a href="#ref-${src}">${src}</a>]`:'');
|
1151 |
+
}
|
1152 |
+
}else{
|
1153 |
+
tooltip.style.display='none';
|
1154 |
+
}
|
1155 |
+
});
|
1156 |
+
// Legend explanation
|
1157 |
+
const legend = container.querySelector('.legend');
|
1158 |
+
legend.innerHTML = `<span class="small muted">Color scale per indicator column (min → max)</span>`;
|
1159 |
+
container.querySelector('.export-png').onclick = ()=> exportCanvasPNG(container.querySelector('canvas'), 'vietnam-forecast-heatmap.png');
|
1160 |
+
container.querySelector('.copy-data').onclick = ()=> copyJSON(mat);
|
1161 |
+
}
|
1162 |
+
|
1163 |
+
/* Export helpers */
|
1164 |
+
function exportCanvasPNG(canvas, filename){
|
1165 |
+
const url = canvas.toDataURL('image/png');
|
1166 |
+
const a = document.createElement('a');
|
1167 |
+
a.href = url; a.download = filename; a.click();
|
1168 |
+
}
|
1169 |
+
async function copyJSON(obj){
|
1170 |
+
try{
|
1171 |
+
await navigator.clipboard.writeText(JSON.stringify(obj, null, 2));
|
1172 |
+
toast('Chart data copied to clipboard');
|
1173 |
+
}catch(e){ console.error(e); }
|
1174 |
+
}
|
1175 |
+
function toast(text){
|
1176 |
+
let t = document.getElementById('toast');
|
1177 |
+
if(!t){
|
1178 |
+
t = document.createElement('div'); t.id='toast';
|
1179 |
+
t.style.position='fixed'; t.style.bottom='20px'; t.style.left='50%'; t.style.transform='translateX(-50%)';
|
1180 |
+
t.style.background='var(--bg-elev)'; t.style.border='1px solid var(--border)'; t.style.padding='10px 12px'; t.style.borderRadius='10px';
|
1181 |
+
t.style.boxShadow='var(--shadow)'; t.style.zIndex='2000'; t.style.color='var(--text)';
|
1182 |
+
document.body.appendChild(t);
|
1183 |
+
}
|
1184 |
+
t.textContent = text; t.style.opacity='1'; t.style.display='block';
|
1185 |
+
setTimeout(()=>{ t.style.transition='opacity .4s'; t.style.opacity='0'; setTimeout(()=>{ t.style.display='none'; t.style.transition=''; }, 400); }, 1300);
|
1186 |
+
}
|
1187 |
+
|
1188 |
+
/* Render charts on load and on resize */
|
1189 |
+
function renderAll(){
|
1190 |
+
renderBar(document.getElementById('chart-gdp-2025'), data.gdp2025);
|
1191 |
+
renderLine(document.getElementById('chart-q1-2020-2025'), data.q1Series);
|
1192 |
+
renderDonut(document.getElementById('chart-fdi-2025'), data.fdi);
|
1193 |
+
renderHeatmap(document.getElementById('chart-forecasts'), data.heatmap);
|
1194 |
+
}
|
1195 |
+
window.addEventListener('load', renderAll);
|
1196 |
+
window.addEventListener('resize', debounce(renderAll, 150));
|
1197 |
+
function debounce(fn, ms){ let t; return (...args)=>{ clearTimeout(t); t=setTimeout(()=>fn.apply(this,args), ms); }; }
|
1198 |
+
|
1199 |
+
/* Data table */
|
1200 |
+
const tableData = [
|
1201 |
+
{cat:'GDP', period:'Q1 2025', indicator:'GDP growth (YoY)', value:6.9, unit:'%', notes:'Quarterly actual', src:4},
|
1202 |
+
{cat:'GDP', period:'Q2 2025', indicator:'GDP growth (YoY)', value:7.96, unit:'%', notes:'Quarterly actual', src:1},
|
1203 |
+
{cat:'GDP', period:'H1 2025', indicator:'GDP growth (YoY)', value:7.52, unit:'%', notes:'Highest H1 since 2011', src:4},
|
1204 |
+
{cat:'GDP', period:'2025 (year)', indicator:'World Bank forecast', value:5.8, unit:'%', notes:'End-year forecast', src:2},
|
1205 |
+
{cat:'GDP', period:'2025 (year)', indicator:'ADB forecast', value:6.6, unit:'%', notes:'End-year forecast', src:16},
|
1206 |
+
{cat:'GDP', period:'2025 (year)', indicator:'IMF forecast', value:5.2, unit:'%', notes:'End-year forecast', src:2},
|
1207 |
+
{cat:'GDP', period:'2025 (year)', indicator:'Government target (mid of 8.3–8.5)', value:8.4, unit:'%', notes:'Target midpoint', src:2},
|
1208 |
+
|
1209 |
+
{cat:'Inflation', period:'May 2025', indicator:'CPI (YoY)', value:3.24, unit:'%', notes:'Monthly actual', src:4},
|
1210 |
+
{cat:'Inflation', period:'June 2025', indicator:'CPI (YoY)', value:3.57, unit:'%', notes:'Monthly actual', src:4},
|
1211 |
+
{cat:'Inflation', period:'2025 (year)', indicator:'IMF forecast', value:2.9, unit:'%', notes:'End-year forecast', src:2},
|
1212 |
+
{cat:'Inflation', period:'2025 (year)', indicator:'ADB forecast', value:4.0, unit:'%', notes:'End-year forecast', src:16},
|
1213 |
+
|
1214 |
+
{cat:'Unemployment', period:'Q1 2025', indicator:'Unemployment rate', value:2.20, unit:'%', notes:'Quarterly actual', src:4},
|
1215 |
+
{cat:'Unemployment', period:'Q4 2024', indicator:'Unemployment rate', value:2.22, unit:'%', notes:'Prior quarter', src:4},
|
1216 |
+
|
1217 |
+
{cat:'FDI', period:'Jan–May 2025', indicator:'Registered capital', value:18.4, unit:'US$ bn', notes:'+51% YoY', src:12},
|
1218 |
+
{cat:'FDI', period:'Jan–May 2025', indicator:'Disbursed capital', value:8.9, unit:'US$ bn', notes:'Realized', src:13},
|
1219 |
+
{cat:'FDI', period:'H1 2025', indicator:'Total FDI', value:21.51, unit:'US$ bn', notes:'+32.6% YoY', src:12},
|
1220 |
+
|
1221 |
+
{cat:'Retail', period:'Q1 2025', indicator:'Retail sales', value:1708, unit:'VND tn', notes:'1.708 quadrillion (~US$66.83bn); +9.9% YoY', src:4},
|
1222 |
+
];
|
1223 |
+
|
1224 |
+
const table = document.getElementById('dataTable').querySelector('tbody');
|
1225 |
+
function renderTable(){
|
1226 |
+
const activeCats = Array.from(document.querySelectorAll('.cat-filter')).filter(c=>c.checked).map(c=>c.value);
|
1227 |
+
localStorage.setItem('activeCats', JSON.stringify(activeCats));
|
1228 |
+
const q = (document.getElementById('searchInput').value || '').toLowerCase();
|
1229 |
+
table.innerHTML='';
|
1230 |
+
const rows = tableData.filter(r=> activeCats.includes(r.cat))
|
1231 |
+
.filter(r => !q || (r.cat+r.period+r.indicator+r.notes).toLowerCase().includes(q));
|
1232 |
+
rows.forEach(r=>{
|
1233 |
+
const tr = document.createElement('tr');
|
1234 |
+
tr.innerHTML = `
|
1235 |
+
<td><span class="tag">${r.cat}</span></td>
|
1236 |
+
<td>${r.period}</td>
|
1237 |
+
<td>${r.indicator}</td>
|
1238 |
+
<td data-sort="${r.value}">${Number.isFinite(r.value) ? r.value : ''}</td>
|
1239 |
+
<td>${r.unit}</td>
|
1240 |
+
<td>${r.notes||''}</td>
|
1241 |
+
<td><a href="#ref-${r.src}">[${r.src}]</a></td>
|
1242 |
+
`;
|
1243 |
+
table.appendChild(tr);
|
1244 |
+
});
|
1245 |
+
}
|
1246 |
+
document.querySelectorAll('.cat-filter').forEach(cb=> cb.addEventListener('change', renderTable));
|
1247 |
+
/* Restore filters */
|
1248 |
+
const savedCats = localStorage.getItem('activeCats');
|
1249 |
+
if(savedCats){
|
1250 |
+
const on = new Set(JSON.parse(savedCats));
|
1251 |
+
document.querySelectorAll('.cat-filter').forEach(cb=> cb.checked = on.has(cb.value));
|
1252 |
+
}
|
1253 |
+
renderTable();
|
1254 |
+
/* Re-render on search input (live filter) */
|
1255 |
+
document.getElementById('searchInput').addEventListener('input', renderTable);
|
1256 |
+
|
1257 |
+
/* Sortable headers */
|
1258 |
+
const thead = document.getElementById('dataTable').querySelector('thead');
|
1259 |
+
let sortState = { idx:0, dir:1 };
|
1260 |
+
thead.addEventListener('click', (e)=>{
|
1261 |
+
const th = e.target.closest('th.sortable'); if(!th) return;
|
1262 |
+
const idx = Array.from(th.parentNode.children).indexOf(th);
|
1263 |
+
sortState.dir = sortState.idx === idx ? -sortState.dir : 1;
|
1264 |
+
sortState.idx = idx;
|
1265 |
+
sortTable(idx, sortState.dir);
|
1266 |
+
});
|
1267 |
+
function sortTable(idx, dir){
|
1268 |
+
const rows = Array.from(table.querySelectorAll('tr'));
|
1269 |
+
rows.sort((a,b)=>{
|
1270 |
+
const ta = a.children[idx].dataset.sort || a.children[idx].textContent.trim();
|
1271 |
+
const tb = b.children[idx].dataset.sort || b.children[idx].textContent.trim();
|
1272 |
+
const na = parseFloat(ta), nb = parseFloat(tb);
|
1273 |
+
if(!isNaN(na) && !isNaN(nb)) return dir*(na-nb);
|
1274 |
+
return dir*ta.localeCompare(tb, undefined, {numeric:true});
|
1275 |
+
});
|
1276 |
+
rows.forEach(r=>table.appendChild(r));
|
1277 |
+
}
|
1278 |
+
|
1279 |
+
/* CSV Export */
|
1280 |
+
document.getElementById('downloadCSV').addEventListener('click', ()=>{
|
1281 |
+
const headers = ['Category','Period','Indicator','Value','Unit','Notes','Source'];
|
1282 |
+
const activeCats = Array.from(document.querySelectorAll('.cat-filter')).filter(c=>c.checked).map(c=>c.value);
|
1283 |
+
const q = (document.getElementById('searchInput').value || '').toLowerCase();
|
1284 |
+
const rows = tableData.filter(r=> activeCats.includes(r.cat))
|
1285 |
+
.filter(r => !q || (r.cat+r.period+r.indicator+r.notes).toLowerCase().includes(q))
|
1286 |
+
.map(r=> [r.cat, r.period, r.indicator, r.value, r.unit, r.notes||'', `ref-${r.src}`]);
|
1287 |
+
const csv = [headers, ...rows].map(r=> r.map(v=> `"${(v??'').toString().replace(/"/g,'""')}"`).join(',')).join('\n');
|
1288 |
+
const blob = new Blob([csv], {type:'text/csv;charset=utf-8;'});
|
1289 |
+
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download='vietnam-2025-dataset.csv'; a.click();
|
1290 |
+
});
|
1291 |
+
|
1292 |
+
/* Utilities: poly for roundRect */
|
1293 |
+
if(!CanvasRenderingContext2D.prototype.roundRect){
|
1294 |
+
CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
|
1295 |
+
const min = Math.min(w, h) / 2; if (r > min) r = min;
|
1296 |
+
this.beginPath();
|
1297 |
+
this.moveTo(x + r, y);
|
1298 |
+
this.arcTo(x + w, y, x + w, y + h, r);
|
1299 |
+
this.arcTo(x + w, y + h, x, y + h, r);
|
1300 |
+
this.arcTo(x, y + h, x, y, r);
|
1301 |
+
this.arcTo(x, y, x + w, y, r);
|
1302 |
+
this.closePath();
|
1303 |
+
return this;
|
1304 |
+
};
|
1305 |
+
}
|
1306 |
+
|
1307 |
+
/* Restore last visited section on load (optional) */
|
1308 |
+
window.addEventListener('load', ()=>{
|
1309 |
+
const last = localStorage.getItem('lastSection');
|
1310 |
+
if(last && !location.hash){
|
1311 |
+
// Do not auto-scroll on load to respect user context
|
1312 |
+
}
|
1313 |
+
});
|
1314 |
+
|
1315 |
+
/* Deep-link highlight on hash change */
|
1316 |
+
function highlightHash(){
|
1317 |
+
const id = location.hash.slice(1);
|
1318 |
+
if(!id) return;
|
1319 |
+
const sec = document.getElementById(id);
|
1320 |
+
if(!sec) return;
|
1321 |
+
sec.animate([{boxShadow:'0 0 0 0 rgba(114,239,221,0)'},{boxShadow:'0 0 0 8px rgba(114,239,221,.25)'}], {duration:450, easing:'ease'});
|
1322 |
+
setTimeout(()=>sec.animate([{boxShadow:'0 0 0 8px rgba(114,239,221,.25)'},{boxShadow:'0 0 0 0 rgba(114,239,221,0)'}], {duration:900, easing:'ease'}), 450);
|
1323 |
+
}
|
1324 |
+
window.addEventListener('hashchange', highlightHash);
|
1325 |
+
highlightHash();
|
1326 |
+
|
1327 |
+
/* Accessibility: focus outlines on keyboard nav */
|
1328 |
+
let usingKeyboard=false;
|
1329 |
+
window.addEventListener('keydown', e=>{ if(e.key==='Tab'){ usingKeyboard=true; document.body.classList.add('kb'); }});
|
1330 |
+
window.addEventListener('mousedown', ()=>{ usingKeyboard=false; document.body.classList.remove('kb'); });
|
1331 |
+
</script>
|
1332 |
+
</body>
|
1333 |
+
</html>
|