thucdangvan020999 commited on
Commit
7b455d2
·
verified ·
1 Parent(s): 5a11065

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +1439 -19
index.html CHANGED
@@ -1,19 +1,1439 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>&nbsp;</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>