Spaces:
Running
Running
Upload index.html with huggingface_hub
Browse files- index.html +1368 -19
index.html
CHANGED
|
@@ -1,19 +1,1368 @@
|
|
| 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" />
|
| 6 |
+
<title>Vietnam Economic Growth Report 2025 — Interactive Research Dashboard</title>
|
| 7 |
+
<meta name="description" content="Modern, responsive dashboard summarizing Vietnam's 2025 economic performance with interactive charts, insights, and citations." />
|
| 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=Material+Symbols+Rounded:[email protected]" rel="stylesheet" />
|
| 11 |
+
<style>
|
| 12 |
+
:root {
|
| 13 |
+
/* Color System */
|
| 14 |
+
--bg: #0b0d12;
|
| 15 |
+
--surface: #121621;
|
| 16 |
+
--elev-1: #161b29;
|
| 17 |
+
--elev-2: #1b2234;
|
| 18 |
+
--text: #e9eef7;
|
| 19 |
+
--muted: #b9c3d9;
|
| 20 |
+
--border: #263049;
|
| 21 |
+
--brand: #4f8cff;
|
| 22 |
+
--accent: #9b7bff;
|
| 23 |
+
--good: #2ecc71;
|
| 24 |
+
--warn: #f5a524;
|
| 25 |
+
--bad: #ff5d5d;
|
| 26 |
+
--info: #2dd4bf;
|
| 27 |
+
|
| 28 |
+
/* Data category colors */
|
| 29 |
+
--gdp: #58d3ff;
|
| 30 |
+
--infl: #ffa557;
|
| 31 |
+
--unemp: #93e38f;
|
| 32 |
+
--fdi: #8bb6ff;
|
| 33 |
+
--retail: #ffd166;
|
| 34 |
+
--risk: #ff7aa2;
|
| 35 |
+
|
| 36 |
+
--shadow: 0 10px 30px rgba(0,0,0,.25);
|
| 37 |
+
|
| 38 |
+
/* Typography scale */
|
| 39 |
+
--step--1: clamp(0.80rem, 0.72rem + 0.4vw, 0.95rem);
|
| 40 |
+
--step-0: clamp(0.95rem, 0.88rem + 0.5vw, 1.05rem);
|
| 41 |
+
--step-1: clamp(1.05rem, 0.95rem + 0.8vw, 1.25rem);
|
| 42 |
+
--step-2: clamp(1.25rem, 1.05rem + 1.0vw, 1.50rem);
|
| 43 |
+
--step-3: clamp(1.50rem, 1.25rem + 1.2vw, 1.95rem);
|
| 44 |
+
--step-4: clamp(1.95rem, 1.60rem + 1.6vw, 2.50rem);
|
| 45 |
+
--radius: 14px;
|
| 46 |
+
--radius-sm: 10px;
|
| 47 |
+
--radius-lg: 22px;
|
| 48 |
+
|
| 49 |
+
/* Layout */
|
| 50 |
+
--sidebar-w: 300px;
|
| 51 |
+
--maxw: 1300px;
|
| 52 |
+
--gap: clamp(12px, 2vw, 22px);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/* Light theme override */
|
| 56 |
+
:root.light {
|
| 57 |
+
--bg: #f7f9fc;
|
| 58 |
+
--surface: #ffffff;
|
| 59 |
+
--elev-1: #f2f5fb;
|
| 60 |
+
--elev-2: #eef2fb;
|
| 61 |
+
--text: #0d1424;
|
| 62 |
+
--muted: #45506b;
|
| 63 |
+
--border: #d9e1f2;
|
| 64 |
+
--shadow: 0 8px 24px rgba(10, 25, 68, 0.08);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
* { box-sizing: border-box; }
|
| 68 |
+
html, body { height: 100%; }
|
| 69 |
+
body {
|
| 70 |
+
margin: 0;
|
| 71 |
+
font-family: "Inter", system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, sans-serif;
|
| 72 |
+
color: var(--text);
|
| 73 |
+
background: radial-gradient(1200px 800px at 80% -10%, rgba(79,140,255,.12), transparent 60%) ,
|
| 74 |
+
radial-gradient(900px 600px at 10% -20%, rgba(157,125,255,.12), transparent 60%),
|
| 75 |
+
var(--bg);
|
| 76 |
+
line-height: 1.6;
|
| 77 |
+
letter-spacing: 0.01em;
|
| 78 |
+
scroll-behavior: smooth;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* Global elements */
|
| 82 |
+
a { color: var(--brand); text-decoration: none; }
|
| 83 |
+
a:hover { text-decoration: underline; }
|
| 84 |
+
img, canvas, svg { max-width: 100%; display: block; }
|
| 85 |
+
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
|
| 86 |
+
.icon { font-family: 'Material Symbols Rounded'; font-variation-settings: 'FILL' 0, 'wght' 500, 'GRAD' 0, 'opsz' 24; vertical-align: middle; }
|
| 87 |
+
.sr-only { position: absolute; width:1px; height:1px; padding:0; overflow:hidden; clip: rect(0,0,0,0); white-space:nowrap; border:0; }
|
| 88 |
+
|
| 89 |
+
/* Top App Bar */
|
| 90 |
+
header.app {
|
| 91 |
+
position: sticky; top: 0; z-index: 1000;
|
| 92 |
+
backdrop-filter: saturate(120%) blur(10px);
|
| 93 |
+
background: color-mix(in hsl, var(--surface) 92%, transparent);
|
| 94 |
+
border-bottom: 1px solid var(--border);
|
| 95 |
+
}
|
| 96 |
+
.progress {
|
| 97 |
+
position: absolute; top: 0; left: 0; height: 3px; width: 0%;
|
| 98 |
+
background: linear-gradient(90deg, var(--brand), var(--accent));
|
| 99 |
+
}
|
| 100 |
+
.app-bar {
|
| 101 |
+
display: grid; grid-template-columns: 1fr auto auto; align-items: center;
|
| 102 |
+
gap: var(--gap); padding: 14px clamp(12px, 4vw, 28px);
|
| 103 |
+
max-width: 1600px; margin-inline: auto;
|
| 104 |
+
}
|
| 105 |
+
.brand {
|
| 106 |
+
display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: center;
|
| 107 |
+
}
|
| 108 |
+
.logo {
|
| 109 |
+
inline-size: 40px; block-size: 40px; border-radius: 12px; display: grid; place-items: center;
|
| 110 |
+
background: conic-gradient(from 200deg, var(--brand), var(--accent));
|
| 111 |
+
box-shadow: var(--shadow);
|
| 112 |
+
}
|
| 113 |
+
.title-wrap h1 { margin: 0; font-size: var(--step-3); font-weight: 800; letter-spacing: -0.01em; }
|
| 114 |
+
.title-wrap p { margin: 2px 0 0; font-size: var(--step--1); color: var(--muted); }
|
| 115 |
+
|
| 116 |
+
.actions { display: flex; align-items: center; gap: 10px; }
|
| 117 |
+
.chip {
|
| 118 |
+
border: 1px solid var(--border); background: var(--elev-1); color: var(--text);
|
| 119 |
+
padding: 8px 12px; border-radius: 999px; font-size: var(--step--1);
|
| 120 |
+
display:flex; align-items:center; gap:8px;
|
| 121 |
+
}
|
| 122 |
+
.btn {
|
| 123 |
+
border: 1px solid var(--border); background: var(--elev-1); color: var(--text);
|
| 124 |
+
padding: 10px 12px; border-radius: 12px; cursor: pointer;
|
| 125 |
+
display: inline-flex; align-items: center; gap: 8px;
|
| 126 |
+
transition: transform .12s ease, background .2s ease, border-color .2s ease;
|
| 127 |
+
}
|
| 128 |
+
.btn:hover { transform: translateY(-1px); background: var(--elev-2); border-color: color-mix(in hsl, var(--border) 80%, var(--brand) 20%); }
|
| 129 |
+
|
| 130 |
+
.search {
|
| 131 |
+
display:flex; align-items:center; gap:8px;
|
| 132 |
+
background: var(--elev-1); border: 1px solid var(--border); border-radius: 12px;
|
| 133 |
+
padding: 8px 12px; min-inline-size: clamp(180px, 35vw, 460px);
|
| 134 |
+
}
|
| 135 |
+
.search input {
|
| 136 |
+
background: transparent; border: none; outline: none; color: var(--text); width: 100%;
|
| 137 |
+
font-size: var(--step-0);
|
| 138 |
+
}
|
| 139 |
+
.search .clear { display:none; cursor:pointer; }
|
| 140 |
+
|
| 141 |
+
/* Layout */
|
| 142 |
+
.shell {
|
| 143 |
+
display: grid;
|
| 144 |
+
grid-template-columns: 1fr;
|
| 145 |
+
max-width: var(--maxw);
|
| 146 |
+
margin-inline: auto;
|
| 147 |
+
gap: var(--gap);
|
| 148 |
+
padding: clamp(12px, 3vw, 28px);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
@media (min-width: 1024px) {
|
| 152 |
+
.shell {
|
| 153 |
+
grid-template-columns: var(--sidebar-w) 1fr;
|
| 154 |
+
align-items: start;
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
/* TOC Sidebar */
|
| 159 |
+
aside.toc {
|
| 160 |
+
position: sticky; top: calc(68px + 3px);
|
| 161 |
+
align-self: start;
|
| 162 |
+
display: none;
|
| 163 |
+
padding-block: 6px;
|
| 164 |
+
max-height: calc(100dvh - 80px);
|
| 165 |
+
overflow: auto;
|
| 166 |
+
}
|
| 167 |
+
@media (min-width: 1024px) { aside.toc { display: block; } }
|
| 168 |
+
|
| 169 |
+
.toc .card {
|
| 170 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 171 |
+
border-radius: var(--radius); padding: 12px; box-shadow: var(--shadow);
|
| 172 |
+
}
|
| 173 |
+
.toc h3 { margin: 8px 8px 12px; font-size: var(--step-0); font-weight: 700; color: var(--muted); }
|
| 174 |
+
.toc a {
|
| 175 |
+
display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: center;
|
| 176 |
+
padding: 10px; border-radius: 10px; color: var(--text); text-decoration: none;
|
| 177 |
+
}
|
| 178 |
+
.toc a .dot { inline-size:8px; block-size:8px; border-radius:50%; background: var(--border); }
|
| 179 |
+
.toc a.active { background: var(--elev-1); }
|
| 180 |
+
.toc a.active .dot { background: var(--brand); }
|
| 181 |
+
.toc small { color: var(--muted); }
|
| 182 |
+
|
| 183 |
+
/* Main content */
|
| 184 |
+
main {
|
| 185 |
+
display: grid; gap: var(--gap);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.hero {
|
| 189 |
+
display: grid; gap: var(--gap);
|
| 190 |
+
container-type: inline-size; container-name: hero;
|
| 191 |
+
}
|
| 192 |
+
.hero-header {
|
| 193 |
+
display: grid; gap: 10px;
|
| 194 |
+
padding: clamp(14px, 2.5vw, 24px);
|
| 195 |
+
border: 1px solid var(--border); border-radius: var(--radius);
|
| 196 |
+
background: linear-gradient(180deg, color-mix(in hsl, var(--surface) 90%, transparent), var(--surface)),
|
| 197 |
+
radial-gradient(600px 300px at 0% -10%, rgba(79,140,255,.12), transparent 60%);
|
| 198 |
+
box-shadow: var(--shadow);
|
| 199 |
+
}
|
| 200 |
+
.hero-header h2 { margin: 0; font-size: var(--step-2); }
|
| 201 |
+
.kpis {
|
| 202 |
+
display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gap);
|
| 203 |
+
}
|
| 204 |
+
@container hero (min-width: 640px) { .kpis { grid-template-columns: repeat(4, 1fr); } }
|
| 205 |
+
.kpi {
|
| 206 |
+
display: grid; gap: 8px; padding: 14px; border:1px solid var(--border); border-radius: 14px;
|
| 207 |
+
background: var(--elev-1); position: relative; isolation: isolate; overflow: clip;
|
| 208 |
+
box-shadow: var(--shadow);
|
| 209 |
+
}
|
| 210 |
+
.kpi::after {
|
| 211 |
+
content:""; position:absolute; inset:auto 0 0 0; height:3px; background: var(--border);
|
| 212 |
+
}
|
| 213 |
+
.kpi[data-type="gdp"]::after{ background: var(--gdp); }
|
| 214 |
+
.kpi[data-type="infl"]::after{ background: var(--infl); }
|
| 215 |
+
.kpi[data-type="unemp"]::after{ background: var(--unemp); }
|
| 216 |
+
.kpi[data-type="fdi"]::after{ background: var(--fdi); }
|
| 217 |
+
.kpi .label { color: var(--muted); font-size: var(--step--1); display:flex; align-items:center; gap:8px; }
|
| 218 |
+
.kpi .value { font-size: var(--step-3); font-weight: 800; letter-spacing: -0.02em; }
|
| 219 |
+
.kpi .delta { font-size: var(--step--1); display:flex; align-items:center; gap:6px; }
|
| 220 |
+
|
| 221 |
+
/* Sections */
|
| 222 |
+
section.section {
|
| 223 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 224 |
+
border-radius: var(--radius); padding: clamp(14px, 2.5vw, 28px);
|
| 225 |
+
box-shadow: var(--shadow);
|
| 226 |
+
scroll-margin-top: 90px;
|
| 227 |
+
}
|
| 228 |
+
section.section h2 {
|
| 229 |
+
margin-top: 0; font-size: var(--step-2);
|
| 230 |
+
display: flex; align-items: center; gap: 10px;
|
| 231 |
+
}
|
| 232 |
+
.grid {
|
| 233 |
+
display: grid; gap: var(--gap);
|
| 234 |
+
}
|
| 235 |
+
.grid.cols-2 { grid-template-columns: 1fr; }
|
| 236 |
+
.grid.cols-3 { grid-template-columns: 1fr; }
|
| 237 |
+
@media (min-width: 768px) {
|
| 238 |
+
.grid.cols-2 { grid-template-columns: repeat(2, 1fr); }
|
| 239 |
+
}
|
| 240 |
+
@media (min-width: 1024px) {
|
| 241 |
+
.grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.card {
|
| 245 |
+
background: var(--elev-1); border: 1px solid var(--border);
|
| 246 |
+
border-radius: var(--radius-sm); padding: clamp(12px, 2vw, 18px);
|
| 247 |
+
display: grid; gap: 10px; box-shadow: var(--shadow);
|
| 248 |
+
container-type: inline-size; container-name: card;
|
| 249 |
+
}
|
| 250 |
+
.card h3 { margin: 0; font-size: var(--step-1); }
|
| 251 |
+
.note { color: var(--muted); font-size: var(--step--1); }
|
| 252 |
+
.pill {
|
| 253 |
+
display: inline-flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 999px;
|
| 254 |
+
border: 1px solid var(--border); background: var(--elev-2); font-size: var(--step--1);
|
| 255 |
+
}
|
| 256 |
+
.stack { display: grid; gap: 8px; }
|
| 257 |
+
.row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
| 258 |
+
|
| 259 |
+
/* Charts */
|
| 260 |
+
.chart {
|
| 261 |
+
aspect-ratio: 16/9;
|
| 262 |
+
border-radius: 12px; overflow: hidden; background: linear-gradient(180deg, var(--elev-2), var(--elev-1));
|
| 263 |
+
border: 1px dashed color-mix(in hsl, var(--border) 80%, var(--brand) 20%);
|
| 264 |
+
}
|
| 265 |
+
.legend { display:flex; flex-wrap: wrap; gap:10px; }
|
| 266 |
+
.legend .item { display:flex; align-items:center; gap:6px; font-size: var(--step--1); color: var(--muted); }
|
| 267 |
+
.legend .swatch { inline-size: 10px; block-size: 10px; border-radius: 2px; }
|
| 268 |
+
|
| 269 |
+
/* Collapsible */
|
| 270 |
+
details.collapsible {
|
| 271 |
+
background: var(--elev-1); border: 1px solid var(--border); border-radius: 10px; padding: 8px 12px;
|
| 272 |
+
}
|
| 273 |
+
details.collapsible[open] { background: var(--elev-2); }
|
| 274 |
+
details.collapsible summary {
|
| 275 |
+
cursor: pointer; list-style: none; display: flex; align-items: center; gap: 10px; font-weight: 600;
|
| 276 |
+
}
|
| 277 |
+
details.collapsible summary::-webkit-details-marker { display: none; }
|
| 278 |
+
|
| 279 |
+
/* Table */
|
| 280 |
+
.table-wrap { overflow: auto; border-radius: 12px; border: 1px solid var(--border); }
|
| 281 |
+
table {
|
| 282 |
+
width: 100%; border-collapse: collapse; background: var(--elev-1);
|
| 283 |
+
}
|
| 284 |
+
th, td { padding: 12px 14px; border-bottom: 1px solid var(--border); text-align: left; }
|
| 285 |
+
th { position: sticky; top: 0; background: var(--elev-2); font-weight: 700; cursor: pointer; }
|
| 286 |
+
tr:hover td { background: color-mix(in hsl, var(--elev-1) 80%, var(--brand) 8%); }
|
| 287 |
+
.badge { font-size: var(--step--1); padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface); }
|
| 288 |
+
|
| 289 |
+
/* Footer */
|
| 290 |
+
footer {
|
| 291 |
+
max-width: var(--maxw); margin: 40px auto; padding: 20px;
|
| 292 |
+
color: var(--muted); text-align: center;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
/* Responsive Helpers */
|
| 296 |
+
.hide-desktop { display: block; }
|
| 297 |
+
@media (min-width: 1024px) { .hide-desktop { display: none; } }
|
| 298 |
+
|
| 299 |
+
/* Highlight search */
|
| 300 |
+
mark.hl { background: color-mix(in hsl, var(--brand) 35%, transparent); padding: 0 2px; border-radius: 4px; }
|
| 301 |
+
|
| 302 |
+
/* Container query variant for compact legends, etc. */
|
| 303 |
+
@container card (max-width: 420px) {
|
| 304 |
+
.legend { display: grid; grid-template-columns: 1fr 1fr; }
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
/* Print */
|
| 308 |
+
@media print {
|
| 309 |
+
header.app, aside.toc, .actions, .btn.print-hide, .search { display: none !important; }
|
| 310 |
+
section.section { break-inside: avoid; }
|
| 311 |
+
body { background: white; color: black; }
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
/* Keyframes */
|
| 315 |
+
@keyframes floatIn { from {opacity:0; transform: translateY(10px)} to {opacity:1; transform: none} }
|
| 316 |
+
.reveal { opacity: 0; transform: translateY(10px); }
|
| 317 |
+
.reveal.in { animation: floatIn .5s ease both; }
|
| 318 |
+
</style>
|
| 319 |
+
</head>
|
| 320 |
+
<body>
|
| 321 |
+
<header class="app">
|
| 322 |
+
<div class="progress" id="progress"></div>
|
| 323 |
+
<div class="app-bar">
|
| 324 |
+
<div class="brand">
|
| 325 |
+
<div class="logo" aria-hidden="true">
|
| 326 |
+
<span class="icon" style="color:white;font-size:22px">query_stats</span>
|
| 327 |
+
</div>
|
| 328 |
+
<div class="title-wrap">
|
| 329 |
+
<h1>Vietnam Economic Growth Report 2025</h1>
|
| 330 |
+
<p>Interactive Research Dashboard · Updated Q2 2025</p>
|
| 331 |
+
</div>
|
| 332 |
+
</div>
|
| 333 |
+
|
| 334 |
+
<div class="search" role="search">
|
| 335 |
+
<span class="icon" aria-hidden="true">search</span>
|
| 336 |
+
<input id="search" type="search" placeholder="Search insights, indicators, or sources…" autocomplete="off" />
|
| 337 |
+
<button class="clear btn" id="clearSearch" title="Clear"><span class="icon">close</span></button>
|
| 338 |
+
</div>
|
| 339 |
+
|
| 340 |
+
<div class="actions">
|
| 341 |
+
<button class="btn" id="tocToggle" title="Toggle Contents (mobile)">
|
| 342 |
+
<span class="icon">menu</span><span class="hide-sm">Contents</span>
|
| 343 |
+
</button>
|
| 344 |
+
<button class="btn" id="themeToggle" title="Toggle theme">
|
| 345 |
+
<span class="icon">dark_mode</span><span class="hide-sm">Theme</span>
|
| 346 |
+
</button>
|
| 347 |
+
<div class="btn" id="exportMenu" title="Export options" style="position:relative">
|
| 348 |
+
<span class="icon">ios_share</span> Export
|
| 349 |
+
<div id="exportPopover" class="card" style="display:none; position:absolute; right:0; top:110%; min-width: 240px; z-index: 1000;">
|
| 350 |
+
<button class="btn" data-export="csv"><span class="icon">table</span> Export dataset (CSV)</button>
|
| 351 |
+
<button class="btn" data-export="png" data-target="chart-gdp-q1-line"><span class="icon">image</span> Export GDP Q1 Line (PNG)</button>
|
| 352 |
+
<button class="btn" data-export="print"><span class="icon">print</span> Print / Save to PDF</button>
|
| 353 |
+
<small class="note">Exports happen locally in your browser.</small>
|
| 354 |
+
</div>
|
| 355 |
+
</div>
|
| 356 |
+
</div>
|
| 357 |
+
</div>
|
| 358 |
+
</header>
|
| 359 |
+
|
| 360 |
+
<div class="shell">
|
| 361 |
+
<aside class="toc" id="toc">
|
| 362 |
+
<div class="card">
|
| 363 |
+
<h3>On this page</h3>
|
| 364 |
+
<nav id="tocList" class="stack" aria-label="Table of contents"></nav>
|
| 365 |
+
</div>
|
| 366 |
+
</aside>
|
| 367 |
+
|
| 368 |
+
<main id="content">
|
| 369 |
+
<section class="hero reveal" id="overview">
|
| 370 |
+
<div class="hero-header">
|
| 371 |
+
<div class="row">
|
| 372 |
+
<h2><span class="icon">insights</span> Executive Overview</h2>
|
| 373 |
+
<span class="pill"><span class="icon">verified</span> Data scope: H1 2025</span>
|
| 374 |
+
</div>
|
| 375 |
+
<p>Vietnam sustains strong momentum into 2025. GDP expanded 7.96% in Q2 (y/y) and 7.52% in H1—the best first-half since 2011—driven by industry and services. Inflation remains contained, unemployment is low, and FDI inflows are robust despite global trade tensions and tariff headwinds.</p>
|
| 376 |
+
</div>
|
| 377 |
+
|
| 378 |
+
<div class="kpis">
|
| 379 |
+
<div class="kpi" data-type="gdp">
|
| 380 |
+
<div class="label"><span class="icon">trending_up</span> GDP Growth (H1 2025)</div>
|
| 381 |
+
<div class="value" data-countup="7.52" data-suffix="%">0%</div>
|
| 382 |
+
<div class="delta" style="color:var(--good)"><span class="icon">arrow_upward</span> Highest H1 since 2011</div>
|
| 383 |
+
</div>
|
| 384 |
+
<div class="kpi" data-type="infl">
|
| 385 |
+
<div class="label"><span class="icon">local_fire_department</span> Inflation (Jun 2025)</div>
|
| 386 |
+
<div class="value" data-countup="3.57" data-suffix="%">0%</div>
|
| 387 |
+
<div class="delta"><span class="icon">flag</span> In target range 3–4.5%</div>
|
| 388 |
+
</div>
|
| 389 |
+
<div class="kpi" data-type="unemp">
|
| 390 |
+
<div class="label"><span class="icon">groups</span> Unemployment (Q1 2025)</div>
|
| 391 |
+
<div class="value" data-countup="2.20" data-suffix="%">0%</div>
|
| 392 |
+
<div class="delta" style="color:var(--good)"><span class="icon">arrow_downward</span> Down from 2.22% (Q4 2024)</div>
|
| 393 |
+
</div>
|
| 394 |
+
<div class="kpi" data-type="fdi">
|
| 395 |
+
<div class="label"><span class="icon">public</span> FDI (H1 2025)</div>
|
| 396 |
+
<div class="value" data-countup="21.51" data-suffix="B">0</div>
|
| 397 |
+
<div class="delta" style="color:var(--good)"><span class="icon">rocket_launch</span> +32.6% y/y</div>
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
</section>
|
| 401 |
+
|
| 402 |
+
<section class="section reveal" id="executive-summary" data-title="Executive Summary">
|
| 403 |
+
<h2><span class="icon">summarize</span> Executive Summary</h2>
|
| 404 |
+
<div class="grid cols-2">
|
| 405 |
+
<div class="card">
|
| 406 |
+
<h3>Key takeaways</h3>
|
| 407 |
+
<ul>
|
| 408 |
+
<li>Q2 GDP up 7.96% y/y; H1 growth 7.52%—strongest since 2011.</li>
|
| 409 |
+
<li>Services and manufacturing are primary growth engines; retail up 9.9% y/y in Q1.</li>
|
| 410 |
+
<li>Inflation remains controlled at 3.57% in June; unemployment at 2.20%.</li>
|
| 411 |
+
<li>FDI momentum robust: US$21.51B in H1; registered capital up 51% in the first five months.</li>
|
| 412 |
+
</ul>
|
| 413 |
+
</div>
|
| 414 |
+
<div class="card">
|
| 415 |
+
<h3>Forecast context</h3>
|
| 416 |
+
<p>International institutions foresee solid but moderating growth: World Bank 5.8%, ADB 6.6%, IMF 5.2%. Government targets are more ambitious at 8.3–8.5%, supported by strong fundamentals and parliamentary support to target at least 8%.</p>
|
| 417 |
+
<p class="note">Interpretation: The gap between targets and external forecasts suggests optimism grounded in domestic resilience but tempered by external risks.</p>
|
| 418 |
+
</div>
|
| 419 |
+
</div>
|
| 420 |
+
<details class="collapsible" open>
|
| 421 |
+
<summary><span class="icon">info</span> Read the concise narrative</summary>
|
| 422 |
+
<p>Vietnam enters 2025 with resilient growth underpinned by services, manufacturing recovery, and steady domestic demand. While global trade tensions and tariffs pose challenges, robust FDI inflows, controlled inflation, and low unemployment provide buffers. Policy focus includes diversifying export markets, bolstering internal demand, and preserving macro-stability, with fiscal space reserved to counter external shocks if needed.</p>
|
| 423 |
+
</details>
|
| 424 |
+
</section>
|
| 425 |
+
|
| 426 |
+
<section class="section reveal" id="methodology" data-title="Methodology & Sources">
|
| 427 |
+
<h2><span class="icon">science</span> Methodology & Sources</h2>
|
| 428 |
+
<div class="grid cols-2">
|
| 429 |
+
<div class="card">
|
| 430 |
+
<h3>Approach</h3>
|
| 431 |
+
<ul class="stack">
|
| 432 |
+
<li>Extracted structured indicators from provided report text.</li>
|
| 433 |
+
<li>Built normalized dataset for GDP, inflation, labor market, FDI, retail, sectors, and risks.</li>
|
| 434 |
+
<li>Visualized with custom Canvas/SVG charts; all processing runs locally in-browser.</li>
|
| 435 |
+
<li>Contextual summaries and cross-references generated to align with the report’s narrative.</li>
|
| 436 |
+
</ul>
|
| 437 |
+
</div>
|
| 438 |
+
<div class="card">
|
| 439 |
+
<h3>Scope & caveats</h3>
|
| 440 |
+
<ul>
|
| 441 |
+
<li>Time horizon: Q1–Q2 2025 (H1) with historical Q1 2020–2025 comparison.</li>
|
| 442 |
+
<li>No external data fetching; citations link to original sources for verification.</li>
|
| 443 |
+
<li>Forecasts reflect listed institutions and government targets in the report.</li>
|
| 444 |
+
</ul>
|
| 445 |
+
</div>
|
| 446 |
+
</div>
|
| 447 |
+
</section>
|
| 448 |
+
|
| 449 |
+
<section class="section reveal" id="gdp" data-title="GDP Growth Performance">
|
| 450 |
+
<h2><span class="icon">show_chart</span> GDP Growth Performance</h2>
|
| 451 |
+
<div class="grid cols-2">
|
| 452 |
+
<div class="card">
|
| 453 |
+
<h3>Q1–Q2 2025 and H1 snapshot</h3>
|
| 454 |
+
<div class="legend">
|
| 455 |
+
<span class="item"><span class="swatch" style="background:var(--gdp)"></span> Actual (y/y)</span>
|
| 456 |
+
</div>
|
| 457 |
+
<canvas class="chart" id="chart-gdp-bars" aria-label="GDP bar chart (Q1, Q2, H1 2025)" role="img"></canvas>
|
| 458 |
+
<p class="note">H1 2025 growth reached 7.52%—best first half since 2011—supported by services and industry.</p>
|
| 459 |
+
</div>
|
| 460 |
+
<div class="card">
|
| 461 |
+
<h3>Q1 growth trend (2020–2025)</h3>
|
| 462 |
+
<div class="legend">
|
| 463 |
+
<span class="item"><span class="swatch" style="background:var(--gdp)"></span> Q1 y/y</span>
|
| 464 |
+
<span class="item"><span class="swatch" style="background:var(--brand)"></span> 2025 highlight</span>
|
| 465 |
+
</div>
|
| 466 |
+
<canvas class="chart" id="chart-gdp-q1-line" aria-label="GDP line chart Q1 2020-2025" role="img"></canvas>
|
| 467 |
+
<p class="note">YoY Q1 growth by year: 3.21%, 4.85%, 5.42%, 3.46%, 5.98%, 6.93%.</p>
|
| 468 |
+
</div>
|
| 469 |
+
</div>
|
| 470 |
+
<div class="card">
|
| 471 |
+
<h3>2025 GDP growth forecasts</h3>
|
| 472 |
+
<div class="legend">
|
| 473 |
+
<span class="item"><span class="swatch" style="background:var(--brand)"></span> International</span>
|
| 474 |
+
<span class="item"><span class="swatch" style="background:var(--accent)"></span> Government target (range)</span>
|
| 475 |
+
</div>
|
| 476 |
+
<canvas class="chart" id="chart-forecasts" aria-label="GDP growth forecasts 2025" role="img"></canvas>
|
| 477 |
+
<p class="note">World Bank: 5.8% · ADB: 6.6% · IMF: 5.2% · Government: 8.3–8.5%.</p>
|
| 478 |
+
</div>
|
| 479 |
+
<div class="card">
|
| 480 |
+
<h3>GDP highlights</h3>
|
| 481 |
+
<ul>
|
| 482 |
+
<li>Services sector is the largest contributor to growth; manufacturing recovery continues.</li>
|
| 483 |
+
<li>Export-oriented industries remain the backbone despite global headwinds.</li>
|
| 484 |
+
<li>Banking sector earnings projected +17% in 2025 on credit growth of ~15%.</li>
|
| 485 |
+
</ul>
|
| 486 |
+
</div>
|
| 487 |
+
</section>
|
| 488 |
+
|
| 489 |
+
<section class="section reveal" id="prices-labor" data-title="Inflation & Labor Market">
|
| 490 |
+
<h2><span class="icon">stacked_bar_chart</span> Inflation & Labor Market</h2>
|
| 491 |
+
<div class="grid cols-2">
|
| 492 |
+
<div class="card">
|
| 493 |
+
<h3>Inflation within target band</h3>
|
| 494 |
+
<div class="legend">
|
| 495 |
+
<span class="item"><span class="swatch" style="background:var(--infl)"></span> Actual</span>
|
| 496 |
+
<span class="item"><span class="swatch" style="background:var(--warn)"></span> Target band (3–4.5%)</span>
|
| 497 |
+
</div>
|
| 498 |
+
<canvas class="chart" id="chart-inflation-gauge" aria-label="Inflation gauge" role="img"></canvas>
|
| 499 |
+
<div class="row">
|
| 500 |
+
<span class="pill"><span class="icon">event</span> May: 3.24% · Jun: 3.57%</span>
|
| 501 |
+
<span class="pill"><span class="icon">insights</span> IMF 2.9% · ADB 4.0% (2025)</span>
|
| 502 |
+
</div>
|
| 503 |
+
</div>
|
| 504 |
+
<div class="card">
|
| 505 |
+
<h3>Labor market remains tight</h3>
|
| 506 |
+
<p>Unemployment stood at 2.20% in Q1 2025, edging down from 2.22% in Q4 2024—historically low and supportive of consumption.</p>
|
| 507 |
+
<div class="row">
|
| 508 |
+
<div class="stack">
|
| 509 |
+
<span class="label">Unemployment</span>
|
| 510 |
+
<div style="display:flex; align-items:center; gap:12px;">
|
| 511 |
+
<strong style="font-size:var(--step-3)">2.20%</strong>
|
| 512 |
+
<span class="badge" style="border-color:var(--unemp); color:var(--unemp)"><span class="icon">check_circle</span> Stable</span>
|
| 513 |
+
</div>
|
| 514 |
+
</div>
|
| 515 |
+
<div style="flex:1"></div>
|
| 516 |
+
</div>
|
| 517 |
+
<details class="collapsible">
|
| 518 |
+
<summary><span class="icon">expand_more</span> Context</summary>
|
| 519 |
+
<p>Low unemployment supports retail resilience and moderates downside risks from external shocks by buffering household demand.</p>
|
| 520 |
+
</details>
|
| 521 |
+
</div>
|
| 522 |
+
</div>
|
| 523 |
+
</section>
|
| 524 |
+
|
| 525 |
+
<section class="section reveal" id="fdi-retail" data-title="FDI & Retail Activity">
|
| 526 |
+
<h2><span class="icon">account_balance</span> FDI & Retail Activity</h2>
|
| 527 |
+
<div class="grid cols-2">
|
| 528 |
+
<div class="card">
|
| 529 |
+
<h3>FDI momentum</h3>
|
| 530 |
+
<div class="legend">
|
| 531 |
+
<span class="item"><span class="swatch" style="background:var(--fdi)"></span> FDI amounts (US$B)</span>
|
| 532 |
+
</div>
|
| 533 |
+
<canvas class="chart" id="chart-fdi" aria-label="FDI chart" role="img"></canvas>
|
| 534 |
+
<ul class="note">
|
| 535 |
+
<li>First 5 months: Registered $18.4B (+51% y/y), Disbursed $8.9B.</li>
|
| 536 |
+
<li>H1 2025 total FDI: $21.51B (+32.6% y/y).</li>
|
| 537 |
+
</ul>
|
| 538 |
+
</div>
|
| 539 |
+
<div class="card">
|
| 540 |
+
<h3>Retail performance</h3>
|
| 541 |
+
<p>Q1 2025 retail sales reached 1.708 quadrillion VND (~US$66.83B), up 9.9% y/y.</p>
|
| 542 |
+
<div class="row">
|
| 543 |
+
<strong style="font-size:var(--step-3)">+9.9% y/y</strong>
|
| 544 |
+
<span class="badge" style="border-color:var(--retail); color:var(--retail)"><span class="icon">shopping_cart</span> Domestic demand</span>
|
| 545 |
+
</div>
|
| 546 |
+
<details class="collapsible">
|
| 547 |
+
<summary><span class="icon">expand_more</span> Implications</summary>
|
| 548 |
+
<p>Healthy retail growth complements export resilience, signaling balanced demand drivers.</p>
|
| 549 |
+
</details>
|
| 550 |
+
</div>
|
| 551 |
+
</div>
|
| 552 |
+
</section>
|
| 553 |
+
|
| 554 |
+
<section class="section reveal" id="risks" data-title="Challenges & Risk Factors">
|
| 555 |
+
<h2><span class="icon">warning</span> Challenges & Risk Factors</h2>
|
| 556 |
+
<div class="grid cols-2">
|
| 557 |
+
<div class="card">
|
| 558 |
+
<h3>Key external pressures</h3>
|
| 559 |
+
<ul>
|
| 560 |
+
<li>Global trade tensions weigh on exports.</li>
|
| 561 |
+
<li>US tariff policies pressure export-oriented businesses.</li>
|
| 562 |
+
<li>Geopolitical instability raises business uncertainty.</li>
|
| 563 |
+
</ul>
|
| 564 |
+
</div>
|
| 565 |
+
<div class="card">
|
| 566 |
+
<h3>Macro considerations</h3>
|
| 567 |
+
<ul>
|
| 568 |
+
<li>Warnings on inflation and potential overdependence on FDI.</li>
|
| 569 |
+
<li>Growth must not undermine macro-stability or debt sustainability.</li>
|
| 570 |
+
</ul>
|
| 571 |
+
</div>
|
| 572 |
+
</div>
|
| 573 |
+
<details class="collapsible">
|
| 574 |
+
<summary><span class="icon">policy</span> Policy posture</summary>
|
| 575 |
+
<p>The government is diversifying export markets, strengthening domestic demand, and preserving macro-stability, with fiscal room to cushion shocks if needed.</p>
|
| 576 |
+
</details>
|
| 577 |
+
</section>
|
| 578 |
+
|
| 579 |
+
<section class="section reveal" id="history" data-title="Historical Comparison">
|
| 580 |
+
<h2><span class="icon">timeline</span> Historical Comparison</h2>
|
| 581 |
+
<div class="grid cols-2">
|
| 582 |
+
<div class="card">
|
| 583 |
+
<h3>Q1 YoY growth (2020–2025)</h3>
|
| 584 |
+
<canvas class="chart" id="chart-gdp-q1-spark" aria-label="Sparkline GDP Q1 2020-2025" role="img"></canvas>
|
| 585 |
+
<p class="note">Sequence: 3.21 → 4.85 → 5.42 → 3.46 → 5.98 → 6.93.</p>
|
| 586 |
+
</div>
|
| 587 |
+
<div class="card">
|
| 588 |
+
<h3>At-a-glance</h3>
|
| 589 |
+
<div class="table-wrap">
|
| 590 |
+
<table>
|
| 591 |
+
<thead>
|
| 592 |
+
<tr><th>Year</th><th>GDP (Q1 y/y)</th><th>Annual Context</th></tr>
|
| 593 |
+
</thead>
|
| 594 |
+
<tbody>
|
| 595 |
+
<tr><td>2020</td><td>3.21%</td><td>Pandemic onset</td></tr>
|
| 596 |
+
<tr><td>2021</td><td>4.85%</td><td>Partial recovery</td></tr>
|
| 597 |
+
<tr><td>2022</td><td>5.42%</td><td>Reopening momentum</td></tr>
|
| 598 |
+
<tr><td>2023</td><td>3.46%</td><td>External slowdown</td></tr>
|
| 599 |
+
<tr><td>2024</td><td>5.98%</td><td>Growth re-acceleration</td></tr>
|
| 600 |
+
<tr><td>2025</td><td>6.93%</td><td>Strong start to the year</td></tr>
|
| 601 |
+
</tbody>
|
| 602 |
+
</table>
|
| 603 |
+
</div>
|
| 604 |
+
<p class="note">2024 annual GDP growth: 7.1%. 2025 may moderate versus H1 pace due to external factors, but fundamentals stay resilient.</p>
|
| 605 |
+
</div>
|
| 606 |
+
</div>
|
| 607 |
+
</section>
|
| 608 |
+
|
| 609 |
+
<section class="section reveal" id="outlook" data-title="Economic Outlook & Projections">
|
| 610 |
+
<h2><span class="icon">crisis_alert</span> Economic Outlook & Projections</h2>
|
| 611 |
+
<div class="grid cols-2">
|
| 612 |
+
<div class="card">
|
| 613 |
+
<h3>Near-term prospects (2025)</h3>
|
| 614 |
+
<ul>
|
| 615 |
+
<li>Solid growth expected amid uncertainty; 8.3–8.5% government target is ambitious.</li>
|
| 616 |
+
<li>Supportive fundamentals: strong FDI, low unemployment, controlled inflation, export competitiveness.</li>
|
| 617 |
+
<li>Parliament raised growth target from 6.5–7% to at least 8%.</li>
|
| 618 |
+
</ul>
|
| 619 |
+
</div>
|
| 620 |
+
<div class="card">
|
| 621 |
+
<h3>Scenarios</h3>
|
| 622 |
+
<div class="stack">
|
| 623 |
+
<label class="row">Assessed growth versus forecasts
|
| 624 |
+
<span class="badge"><span class="icon">target</span> Compare</span>
|
| 625 |
+
</label>
|
| 626 |
+
<div class="legend">
|
| 627 |
+
<span class="item"><span class="swatch" style="background:var(--brand)"></span> Forecasts</span>
|
| 628 |
+
<span class="item"><span class="swatch" style="background:var(--accent)"></span> Government target</span>
|
| 629 |
+
</div>
|
| 630 |
+
<canvas class="chart" id="chart-forecasts-mini" aria-label="Forecast comparison mini chart" role="img"></canvas>
|
| 631 |
+
</div>
|
| 632 |
+
</div>
|
| 633 |
+
</div>
|
| 634 |
+
<details class="collapsible">
|
| 635 |
+
<summary><span class="icon">security</span> Risk mitigation strategies</summary>
|
| 636 |
+
<ul>
|
| 637 |
+
<li>Diversify export markets and supply chains.</li>
|
| 638 |
+
<li>Stimulate domestic demand through targeted measures.</li>
|
| 639 |
+
<li>Enhance economic resilience and productivity.</li>
|
| 640 |
+
<li>Maintain macro-stability; flex fiscal policy if global shocks intensify.</li>
|
| 641 |
+
</ul>
|
| 642 |
+
</details>
|
| 643 |
+
</section>
|
| 644 |
+
|
| 645 |
+
<section class="section reveal" id="dataset" data-title="Interactive Dataset">
|
| 646 |
+
<h2><span class="icon">dataset</span> Interactive Dataset</h2>
|
| 647 |
+
<div class="card">
|
| 648 |
+
<div class="row">
|
| 649 |
+
<div style="display:flex; gap:10px; flex-wrap:wrap">
|
| 650 |
+
<label class="chip">Filter:
|
| 651 |
+
<select id="filterSelect" style="background:transparent; border:none; color:var(--text);">
|
| 652 |
+
<option value="all">All</option>
|
| 653 |
+
<option value="GDP">GDP</option>
|
| 654 |
+
<option value="Inflation">Inflation</option>
|
| 655 |
+
<option value="Labor">Labor</option>
|
| 656 |
+
<option value="FDI">FDI</option>
|
| 657 |
+
<option value="Retail">Retail</option>
|
| 658 |
+
<option value="Sectors">Sectors</option>
|
| 659 |
+
<option value="Risk">Risk</option>
|
| 660 |
+
<option value="Forecast">Forecast</option>
|
| 661 |
+
</select>
|
| 662 |
+
</label>
|
| 663 |
+
<label class="chip">Search:
|
| 664 |
+
<input id="tableSearch" type="text" placeholder="e.g., 7.52 or ADB" style="background:transparent; border:none; outline:none; color:var(--text)" />
|
| 665 |
+
</label>
|
| 666 |
+
</div>
|
| 667 |
+
<div style="display:flex; gap:8px;">
|
| 668 |
+
<button class="btn" id="exportCSV"><span class="icon">table</span> CSV</button>
|
| 669 |
+
<button class="btn" id="resetTable"><span class="icon">refresh</span> Reset</button>
|
| 670 |
+
</div>
|
| 671 |
+
</div>
|
| 672 |
+
<div class="table-wrap">
|
| 673 |
+
<table id="dataTable" aria-label="Key indicators table">
|
| 674 |
+
<thead>
|
| 675 |
+
<tr>
|
| 676 |
+
<th data-sort="category">Category</th>
|
| 677 |
+
<th data-sort="metric">Metric</th>
|
| 678 |
+
<th data-sort="value">Value</th>
|
| 679 |
+
<th data-sort="period">Period</th>
|
| 680 |
+
<th data-sort="source">Source</th>
|
| 681 |
+
</tr>
|
| 682 |
+
</thead>
|
| 683 |
+
<tbody></tbody>
|
| 684 |
+
</table>
|
| 685 |
+
</div>
|
| 686 |
+
<small class="note">Tip: Click headers to sort. Use filters to refine. Export respects active filters.</small>
|
| 687 |
+
</div>
|
| 688 |
+
</section>
|
| 689 |
+
|
| 690 |
+
<section class="section reveal" id="references" data-title="Sources & Citations">
|
| 691 |
+
<h2><span class="icon">link</span> Sources & Citations</h2>
|
| 692 |
+
<div class="grid cols-2" id="citations">
|
| 693 |
+
<!-- Citations populated below -->
|
| 694 |
+
</div>
|
| 695 |
+
<small class="note">Use the copy buttons to capture citations for your notes. Links open in a new tab.</small>
|
| 696 |
+
</section>
|
| 697 |
+
|
| 698 |
+
<section class="section reveal" id="appendix" data-title="Appendices">
|
| 699 |
+
<h2><span class="icon">inventory_2</span> Appendices</h2>
|
| 700 |
+
<div class="grid cols-2">
|
| 701 |
+
<div class="card">
|
| 702 |
+
<h3>Notes & bookmarks</h3>
|
| 703 |
+
<label class="stack">
|
| 704 |
+
<textarea id="notes" rows="6" placeholder="Write your notes here…" style="width:100%; background:var(--surface); color:var(--text); border:1px solid var(--border); border-radius:10px; padding:10px"></textarea>
|
| 705 |
+
<div class="row">
|
| 706 |
+
<span class="note">Saved locally in your browser.</span>
|
| 707 |
+
<button class="btn" id="saveNotes"><span class="icon">bookmark</span> Save notes</button>
|
| 708 |
+
</div>
|
| 709 |
+
</label>
|
| 710 |
+
</div>
|
| 711 |
+
<div class="card">
|
| 712 |
+
<h3>Export & share</h3>
|
| 713 |
+
<div class="stack">
|
| 714 |
+
<button class="btn" onclick="window.print()"><span class="icon">print</span> Print / Save to PDF</button>
|
| 715 |
+
<button class="btn" id="copyLink"><span class="icon">link</span> Copy link to this report</button>
|
| 716 |
+
</div>
|
| 717 |
+
<p class="note" id="copyStatus"></p>
|
| 718 |
+
</div>
|
| 719 |
+
</div>
|
| 720 |
+
</section>
|
| 721 |
+
</main>
|
| 722 |
+
</div>
|
| 723 |
+
|
| 724 |
+
<footer>
|
| 725 |
+
<p>Built with modern web standards: CSS Grid, Flexbox, Container Queries, and Vanilla JS.</p>
|
| 726 |
+
<p>© 2025 Vietnam Economic Insights Dashboard</p>
|
| 727 |
+
</footer>
|
| 728 |
+
|
| 729 |
+
<script>
|
| 730 |
+
// Data model extracted from the report
|
| 731 |
+
const Data = {
|
| 732 |
+
gdp: {
|
| 733 |
+
q1: 6.9,
|
| 734 |
+
q2: 7.96,
|
| 735 |
+
h1: 7.52,
|
| 736 |
+
q1History: {
|
| 737 |
+
labels: ['2020','2021','2022','2023','2024','2025'],
|
| 738 |
+
values: [3.21, 4.85, 5.42, 3.46, 5.98, 6.93]
|
| 739 |
+
},
|
| 740 |
+
forecasts: [
|
| 741 |
+
{ label: 'World Bank', value: 5.8, type: 'intl' },
|
| 742 |
+
{ label: 'ADB', value: 6.6, type: 'intl' },
|
| 743 |
+
{ label: 'IMF', value: 5.2, type: 'intl' },
|
| 744 |
+
{ label: 'Gov Target (min)', value: 8.3, type: 'gov' },
|
| 745 |
+
{ label: 'Gov Target (max)', value: 8.5, type: 'gov' }
|
| 746 |
+
]
|
| 747 |
+
},
|
| 748 |
+
inflation: {
|
| 749 |
+
may: 3.24, jun: 3.57,
|
| 750 |
+
target: { min: 3.0, max: 4.5 },
|
| 751 |
+
forecasts: { IMF: 2.9, ADB: 4.0 }
|
| 752 |
+
},
|
| 753 |
+
labor: { unemploymentQ1: 2.20, prevQ4: 2.22 },
|
| 754 |
+
fdi: {
|
| 755 |
+
first5m: { registered: 18.4, disbursed: 8.9 },
|
| 756 |
+
h1total: 21.51, yoy: 32.6
|
| 757 |
+
},
|
| 758 |
+
retail: { q1vndQuadrillion: 1.708, usdB: 66.83, yoy: 9.9 },
|
| 759 |
+
sectors: [
|
| 760 |
+
{ name: 'Services', note: 'Major contributor' },
|
| 761 |
+
{ name: 'Manufacturing', note: 'Recovery trajectory' },
|
| 762 |
+
{ name: 'Export Industries', note: 'Economic backbone' },
|
| 763 |
+
{ name: 'Banking', note: 'Earnings +17% on credit +15%' }
|
| 764 |
+
],
|
| 765 |
+
risks: [
|
| 766 |
+
'Global trade tensions',
|
| 767 |
+
'US tariff policies',
|
| 768 |
+
'Geopolitical instability',
|
| 769 |
+
'FDI overdependence / inflation risk',
|
| 770 |
+
'Macro-stability, debt, inflation vigilance'
|
| 771 |
+
],
|
| 772 |
+
citations: [
|
| 773 |
+
{ id: 1, title: 'Trading Economics - Vietnam GDP Annual Growth Rate', url: 'https://tradingeconomics.com/vietnam/gdp-growth-annual' },
|
| 774 |
+
{ id: 2, title: 'International Monetary Fund - Vietnam Country Profile', url: 'https://www.imf.org/en/Countries/VNM' },
|
| 775 |
+
{ id: 3, title: 'World Economics - Vietnam GDP Estimates', url: 'https://www.worldeconomics.com/GDP/Vietnam.gdp' },
|
| 776 |
+
{ id: 4, title: 'Government of Vietnam - General Statistics Office', url: 'https://www.gso.gov.vn/en/' },
|
| 777 |
+
{ id: 5, title: 'Wikipedia - Economy of Vietnam', url: 'https://en.wikipedia.org/wiki/Economy_of_Vietnam' },
|
| 778 |
+
{ id: 6, title: 'IMF - Vietnam and the IMF', url: 'https://www.imf.org/en/Countries/VNM' },
|
| 779 |
+
{ id: 7, title: 'FocusEconomics - Vietnam Economic Indicators', url: 'https://www.focus-economics.com/countries/vietnam' },
|
| 780 |
+
{ id: 8, title: 'National Statistics Office of Vietnam - Economic Reports', url: 'https://www.gso.gov.vn/en/data-and-statistics/' },
|
| 781 |
+
{ id: 9, title: 'VietnamNet - Economic News and Analysis', url: 'https://vietnamnet.vn/' },
|
| 782 |
+
{ id: 10, title: 'IMF - Article IV Mission Reports', url: 'https://www.imf.org/en/Publications/CR' },
|
| 783 |
+
{ id: 11, title: 'Vietnam Briefing - Economic Analysis', url: 'https://www.vietnam-briefing.com/' },
|
| 784 |
+
{ id: 12, title: 'Vietnam Investment Review - FDI Statistics', url: 'https://vir.com.vn/' },
|
| 785 |
+
{ id: 13, title: 'Trading Economics - Vietnam Foreign Direct Investment', url: 'https://tradingeconomics.com/vietnam/foreign-direct-investment' },
|
| 786 |
+
{ id: 14, title: 'White & Case - Regional Economic Outlook', url: 'https://www.whitecase.com/' },
|
| 787 |
+
{ id: 15, title: 'Vietnam Economic Times', url: 'https://vneconomictimes.com/' },
|
| 788 |
+
{ id: 16, title: 'Asian Development Bank - Vietnam Country Partnership', url: 'https://www.adb.org/countries/viet-nam/main' },
|
| 789 |
+
{ id: 17, title: 'Ministry of Planning and Investment - Vietnam', url: 'https://www.mpi.gov.vn/en/' }
|
| 790 |
+
]
|
| 791 |
+
};
|
| 792 |
+
|
| 793 |
+
// Theme handling
|
| 794 |
+
const root = document.documentElement;
|
| 795 |
+
const savedTheme = localStorage.getItem('theme');
|
| 796 |
+
if (savedTheme === 'light') root.classList.add('light');
|
| 797 |
+
|
| 798 |
+
document.getElementById('themeToggle').addEventListener('click', () => {
|
| 799 |
+
root.classList.toggle('light');
|
| 800 |
+
localStorage.setItem('theme', root.classList.contains('light') ? 'light' : 'dark');
|
| 801 |
+
});
|
| 802 |
+
|
| 803 |
+
// Progress bar and active section highlighting
|
| 804 |
+
const progressEl = document.getElementById('progress');
|
| 805 |
+
const sections = [...document.querySelectorAll('main section.section, section.hero')];
|
| 806 |
+
const options = { rootMargin: '-20% 0px -70% 0px', threshold: [0, 0.25, 0.5, 0.75, 1] };
|
| 807 |
+
const tocList = document.getElementById('tocList');
|
| 808 |
+
|
| 809 |
+
// Build TOC
|
| 810 |
+
sections.forEach(sec => {
|
| 811 |
+
const title = sec.dataset.title || sec.querySelector('h2')?.innerText || 'Section';
|
| 812 |
+
const id = sec.id || title.toLowerCase().replace(/\s+/g, '-');
|
| 813 |
+
sec.id = id;
|
| 814 |
+
const a = document.createElement('a');
|
| 815 |
+
a.href = `#${id}`;
|
| 816 |
+
a.innerHTML = `<span class="dot" aria-hidden="true"></span><span>${title}</span><small></small>`;
|
| 817 |
+
tocList.appendChild(a);
|
| 818 |
+
});
|
| 819 |
+
|
| 820 |
+
const tocLinks = [...tocList.querySelectorAll('a')];
|
| 821 |
+
|
| 822 |
+
const io = new IntersectionObserver((entries) => {
|
| 823 |
+
entries.forEach(entry => {
|
| 824 |
+
const idx = sections.indexOf(entry.target);
|
| 825 |
+
const link = tocLinks[idx];
|
| 826 |
+
if (entry.isIntersecting) {
|
| 827 |
+
tocLinks.forEach(a => a.classList.remove('active'));
|
| 828 |
+
link?.classList.add('active');
|
| 829 |
+
}
|
| 830 |
+
});
|
| 831 |
+
|
| 832 |
+
// update progress
|
| 833 |
+
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
| 834 |
+
const docH = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
| 835 |
+
const pct = Math.max(0, Math.min(1, scrollTop / docH));
|
| 836 |
+
progressEl.style.width = (pct * 100) + '%';
|
| 837 |
+
}, options);
|
| 838 |
+
|
| 839 |
+
sections.forEach(sec => io.observe(sec));
|
| 840 |
+
|
| 841 |
+
// Reveal on view
|
| 842 |
+
const revealIO = new IntersectionObserver((entries) => {
|
| 843 |
+
entries.forEach(e => {
|
| 844 |
+
if (e.isIntersecting) {
|
| 845 |
+
e.target.classList.add('in');
|
| 846 |
+
revealIO.unobserve(e.target);
|
| 847 |
+
}
|
| 848 |
+
});
|
| 849 |
+
}, { threshold: 0.15 });
|
| 850 |
+
document.querySelectorAll('.reveal').forEach(el => revealIO.observe(el));
|
| 851 |
+
|
| 852 |
+
// Mobile TOC toggle
|
| 853 |
+
document.getElementById('tocToggle').addEventListener('click', () => {
|
| 854 |
+
document.getElementById('toc').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 855 |
+
});
|
| 856 |
+
|
| 857 |
+
// Count-up KPI on view
|
| 858 |
+
function countUp(el, to, suffix = '') {
|
| 859 |
+
const duration = 1200; const start = performance.now();
|
| 860 |
+
const from = 0;
|
| 861 |
+
function frame(now) {
|
| 862 |
+
const p = Math.min(1, (now - start) / duration);
|
| 863 |
+
const val = from + (to - from) * (1 - Math.pow(1 - p, 3));
|
| 864 |
+
el.textContent = (Math.round(val * 100) / 100) + (suffix || '');
|
| 865 |
+
if (p < 1) requestAnimationFrame(frame);
|
| 866 |
+
}
|
| 867 |
+
requestAnimationFrame(frame);
|
| 868 |
+
}
|
| 869 |
+
const kpiIO = new IntersectionObserver((entries) => {
|
| 870 |
+
entries.forEach(e => {
|
| 871 |
+
if (e.isIntersecting) {
|
| 872 |
+
const v = parseFloat(e.target.dataset.countup);
|
| 873 |
+
const suffix = e.target.dataset.suffix || '';
|
| 874 |
+
countUp(e.target, v, suffix);
|
| 875 |
+
kpiIO.unobserve(e.target);
|
| 876 |
+
}
|
| 877 |
+
});
|
| 878 |
+
}, { threshold: 0.6 });
|
| 879 |
+
document.querySelectorAll('.kpi .value').forEach(v => kpiIO.observe(v));
|
| 880 |
+
|
| 881 |
+
// Search within content
|
| 882 |
+
const searchInput = document.getElementById('search');
|
| 883 |
+
const clearSearch = document.getElementById('clearSearch');
|
| 884 |
+
let lastSearch = '';
|
| 885 |
+
|
| 886 |
+
function clearHighlights() {
|
| 887 |
+
document.querySelectorAll('mark.hl').forEach(m => {
|
| 888 |
+
const parent = m.parentNode;
|
| 889 |
+
parent.replaceChild(document.createTextNode(m.textContent), m);
|
| 890 |
+
parent.normalize();
|
| 891 |
+
});
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
function highlightText(node, query) {
|
| 895 |
+
if (!query) return;
|
| 896 |
+
const walk = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null);
|
| 897 |
+
const matcher = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
| 898 |
+
const textNodes = [];
|
| 899 |
+
while (walk.nextNode()) textNodes.push(walk.currentNode);
|
| 900 |
+
textNodes.forEach(t => {
|
| 901 |
+
const val = t.nodeValue;
|
| 902 |
+
if (matcher.test(val)) {
|
| 903 |
+
const frag = document.createDocumentFragment();
|
| 904 |
+
let lastIdx = 0;
|
| 905 |
+
val.replace(matcher, (m, idx) => {
|
| 906 |
+
frag.appendChild(document.createTextNode(val.slice(lastIdx, idx)));
|
| 907 |
+
const mark = document.createElement('mark'); mark.className = 'hl'; mark.textContent = m;
|
| 908 |
+
frag.appendChild(mark);
|
| 909 |
+
lastIdx = idx + m.length;
|
| 910 |
+
});
|
| 911 |
+
frag.appendChild(document.createTextNode(val.slice(lastIdx)));
|
| 912 |
+
t.parentNode.replaceChild(frag, t);
|
| 913 |
+
}
|
| 914 |
+
});
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
searchInput.addEventListener('input', (e) => {
|
| 918 |
+
const q = e.target.value.trim();
|
| 919 |
+
clearSearch.style.display = q ? 'inline-flex' : 'none';
|
| 920 |
+
if (q === lastSearch) return;
|
| 921 |
+
clearHighlights();
|
| 922 |
+
if (q.length >= 2) {
|
| 923 |
+
document.querySelectorAll('main section').forEach(sec => highlightText(sec, q));
|
| 924 |
+
}
|
| 925 |
+
lastSearch = q;
|
| 926 |
+
localStorage.setItem('search', q);
|
| 927 |
+
});
|
| 928 |
+
clearSearch.addEventListener('click', () => {
|
| 929 |
+
searchInput.value = '';
|
| 930 |
+
clearSearch.style.display = 'none';
|
| 931 |
+
clearHighlights();
|
| 932 |
+
localStorage.removeItem('search');
|
| 933 |
+
});
|
| 934 |
+
|
| 935 |
+
// Restore previous search
|
| 936 |
+
const savedSearch = localStorage.getItem('search');
|
| 937 |
+
if (savedSearch) { searchInput.value = savedSearch; clearSearch.style.display = 'inline-flex'; highlightText(document.querySelector('main'), savedSearch); }
|
| 938 |
+
|
| 939 |
+
// Simple Chart Utilities (Canvas 2D)
|
| 940 |
+
function getCtx(id) {
|
| 941 |
+
const canvas = document.getElementById(id);
|
| 942 |
+
const dpr = window.devicePixelRatio || 1;
|
| 943 |
+
const rect = canvas.getBoundingClientRect();
|
| 944 |
+
canvas.width = rect.width * dpr;
|
| 945 |
+
canvas.height = rect.height * dpr;
|
| 946 |
+
const ctx = canvas.getContext('2d');
|
| 947 |
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
| 948 |
+
return ctx;
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
function drawAxes(ctx, padding, maxV, minV=0, labels=[]) {
|
| 952 |
+
const { width, height } = ctx.canvas.getBoundingClientRect();
|
| 953 |
+
const area = { x: padding, y: padding, w: width - padding*2, h: height - padding*2 };
|
| 954 |
+
ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--border');
|
| 955 |
+
ctx.lineWidth = 1;
|
| 956 |
+
// grid lines
|
| 957 |
+
ctx.beginPath();
|
| 958 |
+
const steps = 4;
|
| 959 |
+
for (let i=0;i<=steps;i++) {
|
| 960 |
+
const y = area.y + area.h - (i/steps)*area.h;
|
| 961 |
+
ctx.moveTo(area.x, y); ctx.lineTo(area.x + area.w, y);
|
| 962 |
+
}
|
| 963 |
+
ctx.stroke();
|
| 964 |
+
// x labels
|
| 965 |
+
if (labels.length) {
|
| 966 |
+
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--muted');
|
| 967 |
+
ctx.font = '12px Inter, sans-serif';
|
| 968 |
+
labels.forEach((lab, i) => {
|
| 969 |
+
const x = area.x + (i + 0.5) * (area.w / labels.length);
|
| 970 |
+
ctx.textAlign = 'center'; ctx.fillText(lab, x, area.y + area.h + 16);
|
| 971 |
+
});
|
| 972 |
+
}
|
| 973 |
+
return area;
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
function barChart(id, labels, values, color) {
|
| 977 |
+
const ctx = getCtx(id);
|
| 978 |
+
const maxV = Math.max(...values) * 1.2;
|
| 979 |
+
const area = drawAxes(ctx, 28, maxV, 0, labels);
|
| 980 |
+
const bw = area.w / labels.length * 0.6;
|
| 981 |
+
ctx.fillStyle = color;
|
| 982 |
+
values.forEach((v, i) => {
|
| 983 |
+
const x = area.x + (i + 0.5) * (area.w / labels.length) - bw/2;
|
| 984 |
+
const h = (v / maxV) * area.h;
|
| 985 |
+
const y = area.y + area.h - h;
|
| 986 |
+
const radius = 6;
|
| 987 |
+
// rounded bar
|
| 988 |
+
const w = bw;
|
| 989 |
+
const r = Math.min(radius, h / 2);
|
| 990 |
+
ctx.beginPath();
|
| 991 |
+
ctx.moveTo(x, y + r);
|
| 992 |
+
ctx.arcTo(x, y, x + r, y, r);
|
| 993 |
+
ctx.lineTo(x + w - r, y);
|
| 994 |
+
ctx.arcTo(x + w, y, x + w, y + r, r);
|
| 995 |
+
ctx.lineTo(x + w, y + h);
|
| 996 |
+
ctx.lineTo(x, y + h);
|
| 997 |
+
ctx.closePath();
|
| 998 |
+
ctx.fill();
|
| 999 |
+
});
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
function lineChart(id, labels, values, color, highlightIdx = null) {
|
| 1003 |
+
const ctx = getCtx(id);
|
| 1004 |
+
const maxV = Math.max(...values) * 1.2;
|
| 1005 |
+
const minV = Math.min(...values) * 0.8;
|
| 1006 |
+
const padding = 28;
|
| 1007 |
+
const area = drawAxes(ctx, padding, maxV, minV, labels);
|
| 1008 |
+
const step = area.w / (labels.length - 1);
|
| 1009 |
+
// line
|
| 1010 |
+
ctx.lineWidth = 3;
|
| 1011 |
+
const grad = ctx.createLinearGradient(area.x, area.y, area.x + area.w, area.y);
|
| 1012 |
+
grad.addColorStop(0, color);
|
| 1013 |
+
grad.addColorStop(1, '#9b7bff');
|
| 1014 |
+
ctx.strokeStyle = grad;
|
| 1015 |
+
ctx.beginPath();
|
| 1016 |
+
values.forEach((v, i) => {
|
| 1017 |
+
const x = area.x + i * step;
|
| 1018 |
+
const y = area.y + area.h - ((v - minV) / (maxV - minV)) * area.h;
|
| 1019 |
+
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
| 1020 |
+
});
|
| 1021 |
+
ctx.stroke();
|
| 1022 |
+
// points
|
| 1023 |
+
values.forEach((v, i) => {
|
| 1024 |
+
const x = area.x + i * step;
|
| 1025 |
+
const y = area.y + area.h - ((v - minV) / (maxV - minV)) * area.h;
|
| 1026 |
+
ctx.fillStyle = i === highlightIdx ? getComputedStyle(document.documentElement).getPropertyValue('--brand') : color;
|
| 1027 |
+
ctx.beginPath(); ctx.arc(x, y, i === highlightIdx ? 5 : 3, 0, Math.PI*2); ctx.fill();
|
| 1028 |
+
});
|
| 1029 |
+
// area fill
|
| 1030 |
+
const fillGrad = ctx.createLinearGradient(0, area.y, 0, area.y + area.h);
|
| 1031 |
+
fillGrad.addColorStop(0, 'rgba(88,211,255,0.25)');
|
| 1032 |
+
fillGrad.addColorStop(1, 'rgba(88,211,255,0.02)');
|
| 1033 |
+
ctx.fillStyle = fillGrad;
|
| 1034 |
+
ctx.beginPath();
|
| 1035 |
+
values.forEach((v, i) => {
|
| 1036 |
+
const x = area.x + i * step;
|
| 1037 |
+
const y = area.y + area.h - ((v - minV) / (maxV - minV)) * area.h;
|
| 1038 |
+
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
| 1039 |
+
});
|
| 1040 |
+
ctx.lineTo(area.x + area.w, area.y + area.h);
|
| 1041 |
+
ctx.lineTo(area.x, area.y + area.h);
|
| 1042 |
+
ctx.closePath();
|
| 1043 |
+
ctx.fill();
|
| 1044 |
+
}
|
| 1045 |
+
|
| 1046 |
+
function rangeBarChart(id, items) {
|
| 1047 |
+
const ctx = getCtx(id);
|
| 1048 |
+
const labels = items.map(i => i.label);
|
| 1049 |
+
const ints = items.filter(i => i.type === 'intl');
|
| 1050 |
+
const gov = items.filter(i => i.type === 'gov');
|
| 1051 |
+
const maxV = Math.max(...items.map(i => i.value)) * 1.1;
|
| 1052 |
+
const area = drawAxes(ctx, 28, maxV, 0, labels.map(l => l.replace(' (min)','').replace(' (max)','')));
|
| 1053 |
+
const bw = area.w / labels.length * 0.6;
|
| 1054 |
+
|
| 1055 |
+
items.forEach((it, i) => {
|
| 1056 |
+
const x = area.x + (i + 0.5) * (area.w / labels.length) - bw/2;
|
| 1057 |
+
const h = (it.value / maxV) * area.h;
|
| 1058 |
+
const y = area.y + area.h - h;
|
| 1059 |
+
ctx.fillStyle = it.type === 'intl' ? getComputedStyle(root).getPropertyValue('--brand') : getComputedStyle(root).getPropertyValue('--accent');
|
| 1060 |
+
ctx.beginPath(); ctx.roundRect(x, y, bw, h, 6); ctx.fill();
|
| 1061 |
+
});
|
| 1062 |
+
}
|
| 1063 |
+
|
| 1064 |
+
function miniComparative(id, points) {
|
| 1065 |
+
const ctx = getCtx(id);
|
| 1066 |
+
const labels = points.map(p => p.label);
|
| 1067 |
+
const values = points.map(p => p.value);
|
| 1068 |
+
const maxV = Math.max(...values) * 1.15;
|
| 1069 |
+
const area = drawAxes(ctx, 28, maxV, 0, labels);
|
| 1070 |
+
const bw = area.w / labels.length * 0.5;
|
| 1071 |
+
points.forEach((p, i) => {
|
| 1072 |
+
const x = area.x + (i + 0.5) * (area.w / labels.length) - bw/2;
|
| 1073 |
+
const h = (p.value / maxV) * area.h;
|
| 1074 |
+
const y = area.y + area.h - h;
|
| 1075 |
+
ctx.fillStyle = p.type === 'gov' ? getComputedStyle(root).getPropertyValue('--accent') : getComputedStyle(root).getPropertyValue('--brand');
|
| 1076 |
+
ctx.beginPath(); ctx.roundRect(x, y, bw, h, 6); ctx.fill();
|
| 1077 |
+
ctx.fillStyle = getComputedStyle(root).getPropertyValue('--text');
|
| 1078 |
+
ctx.font = 'bold 12px Inter, sans-serif';
|
| 1079 |
+
ctx.textAlign = 'center';
|
| 1080 |
+
ctx.fillText(p.value + '%', x + bw/2, y - 6);
|
| 1081 |
+
});
|
| 1082 |
+
}
|
| 1083 |
+
|
| 1084 |
+
function gauge(id, value, min, max, bandMin, bandMax) {
|
| 1085 |
+
const ctx = getCtx(id);
|
| 1086 |
+
const { width, height } = ctx.canvas.getBoundingClientRect();
|
| 1087 |
+
const cx = width/2, cy = height*0.82, radius = Math.min(width, height)*0.36;
|
| 1088 |
+
const start = Math.PI, end = 2*Math.PI;
|
| 1089 |
+
|
| 1090 |
+
// background arc
|
| 1091 |
+
ctx.lineWidth = 18; ctx.lineCap = 'round';
|
| 1092 |
+
ctx.strokeStyle = getComputedStyle(root).getPropertyValue('--border');
|
| 1093 |
+
ctx.beginPath(); ctx.arc(cx, cy, radius, start, end); ctx.stroke();
|
| 1094 |
+
|
| 1095 |
+
// target band
|
| 1096 |
+
const map = v => start + (v - min)/(max - min) * (end - start);
|
| 1097 |
+
ctx.strokeStyle = getComputedStyle(root).getPropertyValue('--warn');
|
| 1098 |
+
ctx.beginPath(); ctx.arc(cx, cy, radius, map(bandMin), map(bandMax)); ctx.stroke();
|
| 1099 |
+
|
| 1100 |
+
// value arc
|
| 1101 |
+
ctx.strokeStyle = getComputedStyle(root).getPropertyValue('--infl');
|
| 1102 |
+
ctx.beginPath(); ctx.arc(cx, cy, radius, start, map(value)); ctx.stroke();
|
| 1103 |
+
|
| 1104 |
+
// needle
|
| 1105 |
+
ctx.save();
|
| 1106 |
+
ctx.translate(cx, cy);
|
| 1107 |
+
const angle = map(value) - Math.PI;
|
| 1108 |
+
ctx.rotate(angle);
|
| 1109 |
+
ctx.fillStyle = getComputedStyle(root).getPropertyValue('--infl');
|
| 1110 |
+
ctx.beginPath();
|
| 1111 |
+
ctx.moveTo(-6, 0); ctx.lineTo(radius, 0); ctx.lineTo(-6, 6); ctx.closePath(); ctx.fill();
|
| 1112 |
+
ctx.restore();
|
| 1113 |
+
|
| 1114 |
+
// labels
|
| 1115 |
+
ctx.fillStyle = getComputedStyle(root).getPropertyValue('--text');
|
| 1116 |
+
ctx.font = '600 18px Inter, sans-serif';
|
| 1117 |
+
ctx.textAlign = 'center';
|
| 1118 |
+
ctx.fillText(value.toFixed(2) + '%', cx, cy - radius - 8);
|
| 1119 |
+
ctx.font = '12px Inter, sans-serif';
|
| 1120 |
+
ctx.fillStyle = getComputedStyle(root).getPropertyValue('--muted');
|
| 1121 |
+
ctx.fillText(`Target ${bandMin}–${bandMax}%`, cx, cy - radius + 12);
|
| 1122 |
+
}
|
| 1123 |
+
|
| 1124 |
+
function drawFDI(id) {
|
| 1125 |
+
const ctx = getCtx(id);
|
| 1126 |
+
const labels = ['Reg (5M)', 'Disb (5M)', 'Total (H1)'];
|
| 1127 |
+
const values = [Data.fdi.first5m.registered, Data.fdi.first5m.disbursed, Data.fdi.h1total];
|
| 1128 |
+
barChart(id, labels, values, getComputedStyle(root).getPropertyValue('--fdi'));
|
| 1129 |
+
}
|
| 1130 |
+
|
| 1131 |
+
// Initialize Charts
|
| 1132 |
+
function renderCharts() {
|
| 1133 |
+
barChart('chart-gdp-bars', ['Q1', 'Q2', 'H1'], [Data.gdp.q1, Data.gdp.q2, Data.gdp.h1], getComputedStyle(root).getPropertyValue('--gdp'));
|
| 1134 |
+
lineChart('chart-gdp-q1-line', Data.gdp.q1History.labels, Data.gdp.q1History.values, getComputedStyle(root).getPropertyValue('--gdp'), Data.gdp.q1History.values.length - 1);
|
| 1135 |
+
rangeBarChart('chart-forecasts', Data.gdp.forecasts);
|
| 1136 |
+
miniComparative('chart-forecasts-mini', Data.gdp.forecasts);
|
| 1137 |
+
lineChart('chart-gdp-q1-spark', Data.gdp.q1History.labels, Data.gdp.q1History.values, getComputedStyle(root).getPropertyValue('--gdp'), Data.gdp.q1History.values.length - 1);
|
| 1138 |
+
gauge('chart-inflation-gauge', Data.inflation.jun, 0, 8, Data.inflation.target.min, Data.inflation.target.max);
|
| 1139 |
+
drawFDI('chart-fdi');
|
| 1140 |
+
}
|
| 1141 |
+
window.addEventListener('resize', () => {
|
| 1142 |
+
// Re-render on resize for crispness
|
| 1143 |
+
renderCharts();
|
| 1144 |
+
});
|
| 1145 |
+
window.addEventListener('load', renderCharts);
|
| 1146 |
+
|
| 1147 |
+
// Export popover
|
| 1148 |
+
const exportMenu = document.getElementById('exportMenu');
|
| 1149 |
+
const exportPopover = document.getElementById('exportPopover');
|
| 1150 |
+
exportMenu.addEventListener('click', (e) => {
|
| 1151 |
+
if (e.target.closest('[data-export]')) return; // handled below
|
| 1152 |
+
exportPopover.style.display = exportPopover.style.display === 'none' ? 'grid' : 'none';
|
| 1153 |
+
});
|
| 1154 |
+
document.addEventListener('click', (e) => {
|
| 1155 |
+
if (!exportMenu.contains(e.target)) exportPopover.style.display = 'none';
|
| 1156 |
+
});
|
| 1157 |
+
exportPopover.addEventListener('click', (e) => {
|
| 1158 |
+
const btn = e.target.closest('[data-export]');
|
| 1159 |
+
if (!btn) return;
|
| 1160 |
+
const type = btn.dataset.export;
|
| 1161 |
+
if (type === 'csv') {
|
| 1162 |
+
exportTableToCSV();
|
| 1163 |
+
} else if (type === 'png') {
|
| 1164 |
+
const target = btn.dataset.target;
|
| 1165 |
+
exportCanvasPNG(target);
|
| 1166 |
+
} else if (type === 'print') {
|
| 1167 |
+
window.print();
|
| 1168 |
+
}
|
| 1169 |
+
});
|
| 1170 |
+
|
| 1171 |
+
function exportCanvasPNG(id) {
|
| 1172 |
+
const canvas = document.getElementById(id);
|
| 1173 |
+
if (!canvas) return alert('Chart not found');
|
| 1174 |
+
canvas.toBlob((blob) => {
|
| 1175 |
+
const a = document.createElement('a');
|
| 1176 |
+
a.href = URL.createObjectURL(blob);
|
| 1177 |
+
a.download = `${id}.png`;
|
| 1178 |
+
a.click();
|
| 1179 |
+
URL.revokeObjectURL(a.href);
|
| 1180 |
+
});
|
| 1181 |
+
}
|
| 1182 |
+
|
| 1183 |
+
// Dataset table
|
| 1184 |
+
const tableData = [
|
| 1185 |
+
{ category: 'GDP', metric: 'Q1 growth (y/y)', value: '6.9%', period: 'Q1 2025', source: 'GSO' },
|
| 1186 |
+
{ category: 'GDP', metric: 'Q2 growth (y/y)', value: '7.96%', period: 'Q2 2025', source: 'GSO' },
|
| 1187 |
+
{ category: 'GDP', metric: 'H1 growth', value: '7.52%', period: 'H1 2025', source: 'GSO' },
|
| 1188 |
+
{ category: 'GDP', metric: '2024 annual growth', value: '7.1%', period: '2024', source: 'GSO' },
|
| 1189 |
+
|
| 1190 |
+
{ category: 'Forecast', metric: 'World Bank GDP 2025', value: '5.8%', period: '2025', source: 'WB' },
|
| 1191 |
+
{ category: 'Forecast', metric: 'ADB GDP 2025', value: '6.6%', period: '2025', source: 'ADB' },
|
| 1192 |
+
{ category: 'Forecast', metric: 'IMF GDP 2025', value: '5.2%', period: '2025', source: 'IMF' },
|
| 1193 |
+
{ category: 'Forecast', metric: 'Government target', value: '8.3–8.5%', period: '2025', source: 'Gov' },
|
| 1194 |
+
|
| 1195 |
+
{ category: 'Inflation', metric: 'May CPI', value: '3.24%', period: 'May 2025', source: 'GSO' },
|
| 1196 |
+
{ category: 'Inflation', metric: 'June CPI', value: '3.57%', period: 'Jun 2025', source: 'GSO' },
|
| 1197 |
+
{ category: 'Inflation', metric: 'IMF CPI forecast', value: '2.9%', period: '2025', source: 'IMF' },
|
| 1198 |
+
{ category: 'Inflation', metric: 'ADB CPI forecast', value: '4.0%', period: '2025', source: 'ADB' },
|
| 1199 |
+
|
| 1200 |
+
{ category: 'Labor', metric: 'Unemployment rate', value: '2.20%', period: 'Q1 2025', source: 'GSO' },
|
| 1201 |
+
|
| 1202 |
+
{ category: 'FDI', metric: 'Registered (first 5 months)', value: '$18.4B', period: 'Jan–May 2025', source: 'MPI/GSO' },
|
| 1203 |
+
{ category: 'FDI', metric: 'Disbursed (first 5 months)', value: '$8.9B', period: 'Jan–May 2025', source: 'MPI/GSO' },
|
| 1204 |
+
{ category: 'FDI', metric: 'Total FDI (H1)', value: '$21.51B', period: 'H1 2025', source: 'MPI/GSO' },
|
| 1205 |
+
{ category: 'FDI', metric: 'FDI y/y increase', value: '32.6%', period: 'H1 2025', source: 'MPI/GSO' },
|
| 1206 |
+
|
| 1207 |
+
{ category: 'Retail', metric: 'Retail sales', value: '1.708 quadrillion VND (~$66.83B)', period: 'Q1 2025', source: 'GSO' },
|
| 1208 |
+
{ category: 'Retail', metric: 'Retail growth', value: '9.9% y/y', period: 'Q1 2025', source: 'GSO' },
|
| 1209 |
+
|
| 1210 |
+
{ category: 'Sectors', metric: 'Services', value: 'Major contributor', period: '2025', source: 'Report' },
|
| 1211 |
+
{ category: 'Sectors', metric: 'Manufacturing', value: 'Recovery trajectory', period: '2025', source: 'Report' },
|
| 1212 |
+
{ category: 'Sectors', metric: 'Export industries', value: 'Economic backbone', period: '2025', source: 'Report' },
|
| 1213 |
+
{ category: 'Sectors', metric: 'Banking sector', value: 'Earnings +17% on credit +15%', period: '2025', source: 'Report' },
|
| 1214 |
+
|
| 1215 |
+
{ category: 'Risk', metric: 'Global trade tensions', value: 'Headwind', period: '2025', source: 'Report' },
|
| 1216 |
+
{ category: 'Risk', metric: 'US tariff policies', value: 'Pressure on exports', period: '2025', source: 'Report' },
|
| 1217 |
+
{ category: 'Risk', metric: 'Geopolitical instability', value: 'Uncertainty', period: '2025', source: 'Report' },
|
| 1218 |
+
{ category: 'Risk', metric: 'FDI overdependence', value: 'Inflation risk', period: '2025', source: 'Experts' },
|
| 1219 |
+
{ category: 'Risk', metric: 'Macro-stability', value: 'Debt/inflation vigilance', period: '2025', source: 'Report' }
|
| 1220 |
+
];
|
| 1221 |
+
|
| 1222 |
+
const tbody = document.querySelector('#dataTable tbody');
|
| 1223 |
+
function populateTable(rows) {
|
| 1224 |
+
tbody.innerHTML = '';
|
| 1225 |
+
rows.forEach(r => {
|
| 1226 |
+
const tr = document.createElement('tr');
|
| 1227 |
+
tr.innerHTML = `
|
| 1228 |
+
<td><span class="badge">${r.category}</span></td>
|
| 1229 |
+
<td>${r.metric}</td>
|
| 1230 |
+
<td>${r.value}</td>
|
| 1231 |
+
<td>${r.period}</td>
|
| 1232 |
+
<td>${r.source}</td>
|
| 1233 |
+
`;
|
| 1234 |
+
tbody.appendChild(tr);
|
| 1235 |
+
});
|
| 1236 |
+
}
|
| 1237 |
+
populateTable(tableData);
|
| 1238 |
+
|
| 1239 |
+
// Sorting
|
| 1240 |
+
const ths = document.querySelectorAll('#dataTable th');
|
| 1241 |
+
let sortState = { key: null, dir: 1 };
|
| 1242 |
+
ths.forEach(th => th.addEventListener('click', () => {
|
| 1243 |
+
const key = th.dataset.sort;
|
| 1244 |
+
const dir = (sortState.key === key) ? -sortState.dir : 1;
|
| 1245 |
+
sortState = { key, dir };
|
| 1246 |
+
const sorted = [...tbody.querySelectorAll('tr')].map(tr => ({
|
| 1247 |
+
tr,
|
| 1248 |
+
v: tr.children[[...ths].findIndex(h => h.dataset.sort === key)].textContent.trim()
|
| 1249 |
+
})).sort((a, b) => {
|
| 1250 |
+
const av = a.v.replace(/[%$B,~]/g,''); const bv = b.v.replace(/[%$B,~]/g,'');
|
| 1251 |
+
const na = parseFloat(av), nb = parseFloat(bv);
|
| 1252 |
+
if (!isNaN(na) && !isNaN(nb)) return dir * (na - nb);
|
| 1253 |
+
return dir * a.v.localeCompare(b.v);
|
| 1254 |
+
});
|
| 1255 |
+
tbody.innerHTML = ''; sorted.forEach(o => tbody.appendChild(o.tr));
|
| 1256 |
+
}));
|
| 1257 |
+
|
| 1258 |
+
// Table filter/search
|
| 1259 |
+
const filterSelect = document.getElementById('filterSelect');
|
| 1260 |
+
const tableSearch = document.getElementById('tableSearch');
|
| 1261 |
+
function applyTableFilters() {
|
| 1262 |
+
const cat = filterSelect.value;
|
| 1263 |
+
const q = tableSearch.value.toLowerCase().trim();
|
| 1264 |
+
const filtered = tableData.filter(r => (cat === 'all' || r.category === cat) &&
|
| 1265 |
+
(q === '' || Object.values(r).some(v => String(v).toLowerCase().includes(q))));
|
| 1266 |
+
populateTable(filtered);
|
| 1267 |
+
}
|
| 1268 |
+
filterSelect.addEventListener('change', applyTableFilters);
|
| 1269 |
+
tableSearch.addEventListener('input', applyTableFilters);
|
| 1270 |
+
document.getElementById('resetTable').addEventListener('click', () => {
|
| 1271 |
+
filterSelect.value = 'all'; tableSearch.value = ''; populateTable(tableData);
|
| 1272 |
+
});
|
| 1273 |
+
|
| 1274 |
+
// CSV Export
|
| 1275 |
+
function exportTableToCSV() {
|
| 1276 |
+
const rows = [['Category','Metric','Value','Period','Source']];
|
| 1277 |
+
[...tbody.querySelectorAll('tr')].forEach(tr => {
|
| 1278 |
+
rows.push([...tr.children].map(td => '"' + td.textContent.replace(/"/g, '""') + '"'));
|
| 1279 |
+
});
|
| 1280 |
+
const csv = rows.map(r => r.join(',')).join('\n');
|
| 1281 |
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
| 1282 |
+
const a = document.createElement('a');
|
| 1283 |
+
a.href = URL.createObjectURL(blob);
|
| 1284 |
+
a.download = 'vietnam_economic_dataset_2025.csv';
|
| 1285 |
+
a.click();
|
| 1286 |
+
URL.revokeObjectURL(a.href);
|
| 1287 |
+
}
|
| 1288 |
+
document.getElementById('exportCSV').addEventListener('click', exportTableToCSV);
|
| 1289 |
+
|
| 1290 |
+
// Populate citations
|
| 1291 |
+
const citationsWrap = document.getElementById('citations');
|
| 1292 |
+
Data.citations.forEach(c => {
|
| 1293 |
+
const card = document.createElement('div');
|
| 1294 |
+
card.className = 'card';
|
| 1295 |
+
card.innerHTML = `
|
| 1296 |
+
<h3>${c.id}. ${c.title}</h3>
|
| 1297 |
+
<div class="row">
|
| 1298 |
+
<a href="${c.url}" target="_blank" rel="noopener noreferrer">${c.url}</a>
|
| 1299 |
+
<button class="btn" data-copy="${c.url}"><span class="icon">content_copy</span> Copy</button>
|
| 1300 |
+
</div>
|
| 1301 |
+
`;
|
| 1302 |
+
citationsWrap.appendChild(card);
|
| 1303 |
+
});
|
| 1304 |
+
citationsWrap.addEventListener('click', async (e) => {
|
| 1305 |
+
const btn = e.target.closest('[data-copy]');
|
| 1306 |
+
if (!btn) return;
|
| 1307 |
+
const text = btn.dataset.copy;
|
| 1308 |
+
try {
|
| 1309 |
+
await navigator.clipboard.writeText(text);
|
| 1310 |
+
btn.innerHTML = '<span class="icon">check</span> Copied';
|
| 1311 |
+
setTimeout(() => btn.innerHTML = '<span class="icon">content_copy</span> Copy', 1200);
|
| 1312 |
+
} catch {
|
| 1313 |
+
alert('Clipboard unavailable');
|
| 1314 |
+
}
|
| 1315 |
+
});
|
| 1316 |
+
|
| 1317 |
+
// Notes (Local Storage)
|
| 1318 |
+
const notesEl = document.getElementById('notes');
|
| 1319 |
+
const savedNotes = localStorage.getItem('notes');
|
| 1320 |
+
if (savedNotes) notesEl.value = savedNotes;
|
| 1321 |
+
document.getElementById('saveNotes').addEventListener('click', () => {
|
| 1322 |
+
localStorage.setItem('notes', notesEl.value);
|
| 1323 |
+
const btn = document.getElementById('saveNotes');
|
| 1324 |
+
btn.innerHTML = '<span class="icon">check</span> Saved';
|
| 1325 |
+
setTimeout(() => btn.innerHTML = '<span class="icon">bookmark</span> Save notes', 1200);
|
| 1326 |
+
});
|
| 1327 |
+
|
| 1328 |
+
// Copy link to report
|
| 1329 |
+
document.getElementById('copyLink').addEventListener('click', async () => {
|
| 1330 |
+
try {
|
| 1331 |
+
await navigator.clipboard.writeText(location.href);
|
| 1332 |
+
document.getElementById('copyStatus').textContent = 'Link copied to clipboard.';
|
| 1333 |
+
setTimeout(() => document.getElementById('copyStatus').textContent = '', 1600);
|
| 1334 |
+
} catch {
|
| 1335 |
+
document.getElementById('copyStatus').textContent = 'Could not access clipboard.';
|
| 1336 |
+
}
|
| 1337 |
+
});
|
| 1338 |
+
|
| 1339 |
+
// Smooth scroll offset adjustment for anchor jumps (optional)
|
| 1340 |
+
document.querySelectorAll('a[href^="#"]').forEach(a => {
|
| 1341 |
+
a.addEventListener('click', (e) => {
|
| 1342 |
+
const id = a.getAttribute('href').slice(1);
|
| 1343 |
+
const el = document.getElementById(id);
|
| 1344 |
+
if (!el) return;
|
| 1345 |
+
e.preventDefault();
|
| 1346 |
+
const y = el.getBoundingClientRect().top + window.scrollY - 80;
|
| 1347 |
+
window.scrollTo({ top: y, behavior: 'smooth' });
|
| 1348 |
+
history.pushState({}, '', '#' + id);
|
| 1349 |
+
});
|
| 1350 |
+
});
|
| 1351 |
+
|
| 1352 |
+
// Initialize TOC counts (child items)
|
| 1353 |
+
function updateTocCounts() {
|
| 1354 |
+
sections.forEach((sec, i) => {
|
| 1355 |
+
const count = sec.querySelectorAll('h3').length;
|
| 1356 |
+
const small = tocLinks[i].querySelector('small');
|
| 1357 |
+
if (count) small.textContent = count + ' items';
|
| 1358 |
+
});
|
| 1359 |
+
}
|
| 1360 |
+
updateTocCounts();
|
| 1361 |
+
|
| 1362 |
+
// Accessibility: keyboard for export menu
|
| 1363 |
+
exportMenu.addEventListener('keydown', (e) => {
|
| 1364 |
+
if (e.key === 'Escape') exportPopover.style.display = 'none';
|
| 1365 |
+
});
|
| 1366 |
+
</script>
|
| 1367 |
+
</body>
|
| 1368 |
+
</html>
|