thucdangvan020999 commited on
Commit
b05d270
·
verified ·
1 Parent(s): efc729e

Upload index.html with huggingface_hub

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