gpt5-v7 / index.html
thucdangvan020999's picture
Upload index.html with huggingface_hub
7b455d2 verified
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vietnam Economic Growth Report 2025 — Interactive Research Dashboard</title>
<meta name="description" content="Comprehensive analytical dashboard for Vietnam's 2025 economic performance: GDP, inflation, unemployment, FDI, sectoral analysis, forecasts, and citations." />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://unpkg.com/feather-icons"></script>
<style>
:root {
--bg: #0b0f14;
--panel: #11161d;
--elev: #0f141b;
--text: #eaf2ff;
--muted: #a8b2c2;
--accent: #3ab5ff;
--accent-2: #00ffa3;
--accent-3: #ff7a59;
--bdr: rgba(255,255,255,0.08);
--ok: #2ecc71;
--warn: #f1c40f;
--err: #e74c3c;
--shadow: 0 10px 30px rgba(0,0,0,0.35), 0 1px 0 rgba(255,255,255,0.02) inset;
--radius: 14px;
--radius-sm: 10px;
--radius-lg: 22px;
--space-1: 0.35rem;
--space-2: 0.6rem;
--space-3: 0.9rem;
--space-4: 1.25rem;
--space-5: 2rem;
--maxw: 1400px;
--toc-w: 280px;
--font-size: clamp(15px, 1.25vw, 17px);
--h1: clamp(1.8rem, 5vw, 3rem);
--h2: clamp(1.3rem, 3.2vw, 2rem);
--h3: clamp(1.15rem, 2.2vw, 1.4rem);
--kbd-bg: #0a0d12;
--link: #7cc8ff;
--mark: rgba(255, 235, 59, .25);
--chip: #1a2330;
}
[data-theme="light"] {
--bg: #f6f8fb;
--panel: #ffffff;
--elev: #ffffff;
--text: #0f1726;
--muted: #475569;
--accent: #0066ff;
--accent-2: #10b981;
--accent-3: #ff5a3c;
--bdr: rgba(0,0,0,0.08);
--shadow: 0 8px 28px rgba(12, 17, 29, 0.07), 0 1px 0 rgba(0,0,0,0.03) inset;
--kbd-bg: #ecf1f8;
--link: #0056d6;
--chip: #eef4ff;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0; font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: radial-gradient(1200px 600px at 10% -10%, rgba(58,181,255,0.15), transparent 40%),
radial-gradient(900px 500px at 110% 10%, rgba(0,255,163,0.12), transparent 40%),
var(--bg);
color: var(--text); font-size: var(--font-size); line-height: 1.5;
}
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }
kbd {
background: var(--kbd-bg); border: 1px solid var(--bdr);
border-bottom-width: 2px; border-radius: 6px; padding: 0 .35rem; font-size: .85em;
box-shadow: var(--shadow);
}
.wrap {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-5);
max-width: var(--maxw);
margin: 0 auto;
padding: clamp(10px, 1.8vw, 22px);
}
header.app {
position: sticky; top: 0; z-index: 50; backdrop-filter: saturate(1.2) blur(10px);
background: color-mix(in hsl, var(--bg) 70%, transparent);
border-bottom: 1px solid var(--bdr);
}
.topbar {
max-width: var(--maxw);
margin: 0 auto; padding: .6rem clamp(10px, 1.8vw, 22px); display: flex; gap: .75rem; align-items: center; justify-content: space-between;
}
.brand { display: flex; align-items: center; gap: .75rem; font-weight: 800; letter-spacing: .2px; }
.brand .logo {
width: 36px; height: 36px; display: grid; place-items: center; border-radius: 10px;
background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: white;
box-shadow: 0 10px 20px color-mix(in oklab, var(--accent) 25%, transparent);
}
.toolbar { display: flex; align-items: center; gap: .5rem; }
.searchbar {
display: flex; align-items: center; gap: .5rem; background: var(--panel);
border: 1px solid var(--bdr); border-radius: 999px; padding: .35rem .65rem; width: min(540px, 55vw);
box-shadow: var(--shadow);
}
.searchbar input {
border: none; outline: none; background: transparent; width: 100%; color: var(--text);
font-size: .95rem;
}
.btn {
display: inline-flex; align-items: center; gap: .5rem; border: 1px solid var(--bdr);
background: var(--panel); color: var(--text); padding: .45rem .75rem; border-radius: 10px; cursor: pointer;
box-shadow: var(--shadow);
}
.btn.primary { background: linear-gradient(180deg, color-mix(in oklab, var(--accent) 18%, transparent), transparent), var(--panel); border-color: color-mix(in oklab, var(--accent) 35%, var(--bdr)); }
.btn.ghost { background: transparent; }
.btn:active { transform: translateY(1px); }
.progressbar {
position: absolute; left: 0; bottom: 0; height: 3px; width: 0%;
background: linear-gradient(90deg, var(--accent), var(--accent-2)); transition: width .2s ease;
}
.layout {
display: grid; grid-template-columns: 1fr; gap: var(--space-5);
}
aside.toc {
position: sticky; top: 70px; align-self: start; border: 1px solid var(--bdr);
background: var(--panel); border-radius: var(--radius); padding: var(--space-4);
box-shadow: var(--shadow);
max-height: calc(100vh - 90px); overflow: auto;
}
.toc h4 { margin: 0 0 .5rem 0; font-size: .95rem; color: var(--muted); display: flex; align-items: center; gap: .4rem; }
.toc nav a {
display: block; padding: .4rem .5rem; margin: .1rem 0; border-radius: 8px; color: var(--text);
text-decoration: none; border: 1px solid transparent;
}
.toc nav a:hover { background: color-mix(in oklab, var(--accent) 16%, transparent); }
.toc nav a.active {
border-color: color-mix(in oklab, var(--accent) 40%, transparent);
background: color-mix(in oklab, var(--accent) 10%, transparent);
}
.main {
display: grid; gap: var(--space-5);
}
.hero {
display: grid; gap: var(--space-4); border: 1px solid var(--bdr);
background: linear-gradient(160deg, color-mix(in oklab, var(--accent) 6%, transparent), transparent 60%), var(--panel);
border-radius: var(--radius-lg); padding: clamp(14px, 2.2vw, 30px); box-shadow: var(--shadow);
}
.hero h1 { font-size: var(--h1); line-height: 1.1; margin: 0; letter-spacing: -0.02em; }
.subhead { color: var(--muted); font-weight: 500; }
.kpi-grid {
display: grid; gap: var(--space-4);
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
container-type: inline-size; container-name: kpi;
}
.kpi {
background: var(--elev); border: 1px solid var(--bdr); border-radius: 14px; padding: var(--space-4);
display: grid; gap: .5rem; position: relative; overflow: clip; box-shadow: var(--shadow);
}
.kpi .value { font-size: clamp(1.4rem, 4vw, 2rem); font-weight: 800; letter-spacing: -0.02em; }
.kpi .label { color: var(--muted); font-weight: 600; font-size: .9rem; }
.kpi .delta { font-size: .85rem; display: inline-flex; align-items: center; gap: .3rem; border-radius: 999px; padding: .15rem .5rem; background: color-mix(in oklab, var(--accent-2) 12%, transparent); color: var(--accent-2); }
.kpi .actions { margin-top: .35rem; display: flex; gap: .35rem; flex-wrap: wrap; }
.kpi .bg {
pointer-events: none; content: ""; position: absolute; inset: auto -20% -35% auto; width: 140px; height: 140px;
background: radial-gradient(60% 60% at 50% 50%, color-mix(in oklab, var(--accent) 35%, transparent), transparent);
border-radius: 50%;
opacity: .35;
}
@container kpi (min-width: 240px) {
.kpi .value { font-size: clamp(1.6rem, 3.4cqi, 2.2rem); }
}
.section {
border: 1px solid var(--bdr); background: var(--panel); border-radius: var(--radius);
padding: clamp(14px, 2vw, 26px); box-shadow: var(--shadow);
}
.section h2 { font-size: var(--h2); line-height: 1.15; margin: 0 0 .5rem 0; scroll-margin-top: 80px; }
.section .summary {
background: color-mix(in oklab, var(--accent) 10%, transparent); border: 1px dashed color-mix(in oklab, var(--accent) 45%, var(--bdr));
padding: .75rem .9rem; border-radius: 12px; color: var(--text);
}
.section .chips { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .5rem; }
.chip {
background: var(--chip); color: var(--text); padding: .35rem .6rem; border-radius: 999px; border: 1px solid var(--bdr);
font-size: .85rem; display: inline-flex; align-items: center; gap: .35rem;
}
.with-toolbar { display: grid; gap: var(--space-3); }
.toolbar-row { display: flex; align-items: center; justify-content: space-between; gap: .75rem; flex-wrap: wrap; }
.tools { display: flex; gap: .5rem; flex-wrap: wrap; }
.grid {
display: grid; gap: var(--space-4);
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.layout { grid-template-columns: minmax(0, 1fr) 300px; }
.grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
.layout { grid-template-columns: 280px minmax(0, 1fr); column-gap: var(--space-5); }
.grid.cols-3-xl { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1440px) {
.layout { grid-template-columns: var(--toc-w) minmax(0, 1fr); }
}
.chart-card {
background: var(--elev); border: 1px solid var(--bdr); border-radius: var(--radius); padding: 1rem;
display: grid; gap: .5rem; box-shadow: var(--shadow);
container-type: inline-size; container-name: chart;
}
.chart-card h3 { margin: 0; font-size: var(--h3); }
.chart {
width: 100%; aspect-ratio: 16 / 9; background:
linear-gradient(0deg, transparent 24%, color-mix(in oklab, var(--text) 2%, transparent) 25% 26%, transparent 27%),
linear-gradient(90deg, transparent 24%, color-mix(in oklab, var(--text) 2%, transparent) 25% 26%, transparent 27%);
background-size: 40px 40px;
border-radius: 12px; border: 1px solid var(--bdr); display: grid; place-items: center; position: relative; overflow: hidden;
}
.legend { display: flex; flex-wrap: wrap; gap: .6rem; font-size: .85rem; color: var(--muted); }
.legend .key { display: inline-flex; align-items: center; gap: .35rem; }
.legend .sw { width: 10px; height: 10px; border-radius: 50%; }
.collapsible {
border: 1px solid var(--bdr); border-radius: 12px; overflow: hidden;
background: var(--elev);
}
.collapsible summary {
list-style: none; cursor: pointer; padding: .75rem 1rem; font-weight: 600;
display: flex; align-items: center; justify-content: space-between; gap: .5rem;
border-bottom: 1px solid var(--bdr);
}
.collapsible summary::-webkit-details-marker { display: none; }
.collapsible .content { padding: 1rem; }
.table-wrap { overflow: auto; border: 1px solid var(--bdr); border-radius: 12px; background: var(--elev); }
table {
width: 100%; border-collapse: collapse; min-width: 680px;
}
th, td {
text-align: left; padding: .7rem .9rem; border-bottom: 1px solid var(--bdr);
}
th {
position: sticky; top: 0; background: linear-gradient(180deg, color-mix(in oklab, var(--panel) 65%, transparent), transparent), var(--elev);
font-weight: 700; user-select: none; cursor: pointer;
}
tr:hover td { background: color-mix(in oklab, var(--accent) 7%, transparent); }
.notice {
background: color-mix(in oklab, var(--warn) 18%, transparent);
border: 1px solid color-mix(in oklab, var(--warn) 40%, var(--bdr));
padding: .75rem .9rem; border-radius: 12px; color: var(--text);
}
.info {
background: color-mix(in oklab, var(--accent) 12%, transparent);
border: 1px solid color-mix(in oklab, var(--accent) 42%, var(--bdr));
padding: .75rem .9rem; border-radius: 12px;
}
.foot {
display: grid; gap: .75rem; color: var(--muted);
border-top: 1px solid var(--bdr); padding: var(--space-4) 0 var(--space-5);
}
.pill {
display: inline-flex; align-items: center; gap: .4rem; padding: .2rem .5rem; border-radius: 999px;
background: color-mix(in oklab, var(--accent-2) 12%, transparent);
border: 1px solid color-mix(in oklab, var(--accent-2) 40%, var(--bdr));
color: var(--text); font-size: .85rem;
}
.meta-row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
.small { font-size: .88rem; color: var(--muted); }
.highlight { background: var(--mark); border-radius: 4px; padding: 0 .15rem; }
.sticky-tools {
position: sticky; bottom: 12px; z-index: 40; display: grid; justify-items: end;
}
.floater {
display: inline-flex; gap: .5rem; padding: .5rem; background: var(--panel); border: 1px solid var(--bdr); border-radius: 14px;
box-shadow: var(--shadow);
}
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
@media (max-width: 480px) {
.searchbar { width: 100%; }
.brand .title { display: none; }
}
</style>
</head>
<body>
<header class="app">
<div class="topbar">
<div class="brand">
<div class="logo" aria-hidden="true"><i data-feather="activity"></i></div>
<div class="title">Vietnam Economic Growth Report 2025</div>
</div>
<div class="toolbar">
<div class="searchbar" role="search">
<i data-feather="search" aria-hidden="true"></i>
<input id="globalSearch" type="search" placeholder="Search the report (Press / to focus)" aria-label="Search" />
<button class="btn ghost" id="clearSearch" title="Clear search"><i data-feather="x-circle"></i></button>
</div>
<button class="btn" id="themeToggle" title="Toggle theme">
<i data-feather="moon"></i>
<span class="hide@sm">Theme</span>
</button>
<button class="btn primary" id="printBtn" title="Export to PDF">
<i data-feather="download-cloud"></i>
Export
</button>
</div>
</div>
<div class="progressbar" id="progressbar"></div>
</header>
<main class="wrap">
<div class="layout">
<aside class="toc" id="toc">
<h4><i data-feather="list"></i> On this page</h4>
<nav id="tocLinks"></nav>
<div class="small" style="margin-top:.75rem;">Tip: Press / to search. Use ↑ ↓ to navigate results.</div>
</aside>
<div class="main">
<section class="hero">
<h1>Vietnam 2025: Momentum With Vigilance</h1>
<div class="subhead">GDP surged 7.96% in Q2 and 7.52% in H1—best first-half since 2011—powered by industry and services. Inflation is contained, unemployment remains low, and FDI inflows are robust amid global headwinds.</div>
<div class="kpi-grid" id="kpiGrid">
<div class="kpi">
<div class="bg"></div>
<div class="label">GDP Growth Q2 2025 (y/y)</div>
<div class="value" data-kpi="gdp_q2">7.96%</div>
<div class="delta"><i data-feather="trending-up"></i> Above Q1</div>
<div class="actions">
<button class="btn ghost copy-kpi" data-copy="GDP Q2 2025: 7.96%">Copy</button>
<button class="btn ghost link-kpi" data-link="#gdp-indicators">Jump</button>
</div>
</div>
<div class="kpi">
<div class="bg"></div>
<div class="label">GDP Growth H1 2025</div>
<div class="value" data-kpi="gdp_h1">7.52%</div>
<div class="delta"><i data-feather="award"></i> Highest since 2011</div>
<div class="actions">
<button class="btn ghost copy-kpi" data-copy="GDP H1 2025: 7.52%">Copy</button>
<button class="btn ghost link-kpi" data-link="#gdp-indicators">Jump</button>
</div>
</div>
<div class="kpi">
<div class="bg"></div>
<div class="label">Inflation (June 2025)</div>
<div class="value">3.57%</div>
<div class="delta" style="color:var(--warn); background: color-mix(in oklab, var(--warn) 14%, transparent);">
<i data-feather="chevron-up"></i> Highest YTD
</div>
<div class="actions">
<button class="btn ghost copy-kpi" data-copy="Inflation June 2025: 3.57%">Copy</button>
<button class="btn ghost link-kpi" data-link="#inflation">Jump</button>
</div>
</div>
<div class="kpi">
<div class="bg"></div>
<div class="label">Unemployment (Q1 2025)</div>
<div class="value">2.20%</div>
<div class="delta" style="color:var(--ok); background: color-mix(in oklab, var(--ok) 14%, transparent);">
<i data-feather="trending-down"></i> From Q4 2024
</div>
<div class="actions">
<button class="btn ghost copy-kpi" data-copy="Unemployment Q1 2025: 2.20%">Copy</button>
<button class="btn ghost link-kpi" data-link="#labor">Jump</button>
</div>
</div>
<div class="kpi">
<div class="bg"></div>
<div class="label">FDI (H1 2025)</div>
<div class="value">$21.51B</div>
<div class="delta" style="color:var(--accent-2); background: color-mix(in oklab, var(--accent-2) 14%, transparent);">
<i data-feather="plus-circle"></i> +32.6% y/y
</div>
<div class="actions">
<button class="btn ghost copy-kpi" data-copy="FDI H1 2025: $21.51B (+32.6% y/y)">Copy</button>
<button class="btn ghost link-kpi" data-link="#fdi">Jump</button>
</div>
</div>
<div class="kpi">
<div class="bg"></div>
<div class="label">Retail Sales (Q1 2025)</div>
<div class="value">₫1,708T</div>
<div class="delta"><i data-feather="shopping-bag"></i> +9.9% y/y</div>
<div class="actions">
<button class="btn ghost copy-kpi" data-copy="Retail Q1 2025: ₫1,708T (+9.9% y/y)">Copy</button>
<button class="btn ghost link-kpi" data-link="#sectoral">Jump</button>
</div>
</div>
</div>
</section>
<section class="section with-toolbar" id="executive-summary" data-title="Executive Summary">
<div class="toolbar-row">
<h2>Executive Summary</h2>
<div class="tools">
<button class="btn ghost copy-section" data-target="executive-summary"><i data-feather="clipboard"></i> Copy</button>
<button class="btn ghost link-section" data-target="executive-summary"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<div class="summary">
Vietnam’s economy sustained robust momentum in 2025. GDP expanded 7.96% y/y in Q2 and 7.52% in H1 (best since 2011). Growth was anchored by services and manufacturing, while inflation remained within the 3–4.5% target band. Unemployment stayed historically low at 2.20%, and FDI inflows accelerated, signaling strong investor confidence despite external frictions from trade tensions and tariffs.
</div>
<ul>
<li>Growth leadership: services, manufacturing, and export industries; banking earnings seen up 17% with ~15% credit growth.</li>
<li>Risks: global trade tensions, tariff exposure, geopolitical uncertainty, and FDI overdependence concerns.</li>
<li>Policy stance: diversify markets, support domestic demand, preserve macro stability, and use fiscal space if needed.</li>
</ul>
</section>
<section class="section with-toolbar" id="gdp-indicators" data-title="Key Economic Indicators">
<div class="toolbar-row">
<h2>Key Economic Indicators 2025</h2>
<div class="tools">
<button class="btn ghost" id="downloadData"><i data-feather="download"></i> Data CSV</button>
<button class="btn ghost link-section" data-target="gdp-indicators"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<div class="grid cols-2">
<div class="chart-card">
<h3>GDP Growth Performance</h3>
<div class="legend">
<span class="key"><span class="sw" style="background:var(--accent)"></span> Actual</span>
<span class="key"><span class="sw" style="background:var(--warn)"></span> Gov Target 8.3–8.5%</span>
</div>
<div class="chart" id="chart-gdp-bars" role="img" aria-label="Bar chart of GDP growth for Q1, Q2, and H1 2025"></div>
<div class="tools">
<button class="btn ghost export-chart" data-chart="chart-gdp-bars"><i data-feather="image"></i> Save PNG</button>
<div class="chip"><i data-feather="filter"></i>
<label style="display:inline-flex;gap:.35rem;align-items:center;">
<input type="checkbox" id="toggleTargetBand" checked /> Target band
</label>
</div>
</div>
</div>
<div class="chart-card">
<h3>2025 GDP Growth Forecasts</h3>
<div class="legend">
<span class="key"><span class="sw" style="background:var(--accent-3)"></span> International</span>
<span class="key"><span class="sw" style="background:var(--accent-2)"></span> Government target</span>
</div>
<div class="chart" id="chart-forecasts" role="img" aria-label="Dot plot of GDP forecasts"></div>
<div class="tools">
<div class="chip"><i data-feather="sliders"></i> Min forecast
<input type="range" id="forecastMin" min="0" max="9" step="0.1" value="0" style="width:160px;">
<span id="forecastMinVal" class="small">0%</span>
</div>
<button class="btn ghost export-chart" data-chart="chart-forecasts"><i data-feather="image"></i> Save PNG</button>
</div>
</div>
</div>
<details class="collapsible" style="margin-top:1rem;">
<summary>
<span>Indicator Table (Sortable)</span>
<i data-feather="chevron-down"></i>
</summary>
<div class="content">
<div class="table-wrap">
<table id="tblIndicators">
<thead>
<tr>
<th data-sort="text">Indicator</th>
<th data-sort="text">Period</th>
<th data-sort="num">Value</th>
<th data-sort="text">Notes</th>
</tr>
</thead>
<tbody>
<tr><td>GDP Growth</td><td>Q1 2025</td><td data-v="6.9">6.9%</td><td>y/y</td></tr>
<tr><td>GDP Growth</td><td>Q2 2025</td><td data-v="7.96">7.96%</td><td>y/y</td></tr>
<tr><td>GDP Growth</td><td>H1 2025</td><td data-v="7.52">7.52%</td><td>Highest since 2011</td></tr>
<tr><td>Inflation</td><td>May 2025</td><td data-v="3.24">3.24%</td><td>YTD</td></tr>
<tr><td>Inflation</td><td>June 2025</td><td data-v="3.57">3.57%</td><td>YTD high</td></tr>
<tr><td>Unemployment</td><td>Q1 2025</td><td data-v="2.2">2.20%</td><td>Down from 2.22%</td></tr>
<tr><td>FDI Registered</td><td>Jan–May 2025</td><td data-v="18.4">$18.4B</td><td>+51% y/y</td></tr>
<tr><td>FDI Disbursed</td><td>Jan–May 2025</td><td data-v="8.9">$8.9B</td><td>&nbsp;</td></tr>
<tr><td>FDI Total</td><td>H1 2025</td><td data-v="21.51">$21.51B</td><td>+32.6% y/y</td></tr>
</tbody>
</table>
</div>
</div>
</details>
</section>
<section class="section with-toolbar" id="inflation" data-title="Inflation Dynamics">
<div class="toolbar-row">
<h2>Inflation Rate</h2>
<div class="tools">
<span class="pill"><i data-feather="target"></i> Target: 3–4.5%</span>
<button class="btn ghost link-section" data-target="inflation"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<p>Inflation remains well-controlled within the 3–4.5% target range. June’s 3.57% is the highest YTD but consistent with a manageable trajectory. External cost pressures from trade tensions are a watch point.</p>
<div class="grid cols-2">
<div class="chart-card">
<h3>Inflation Recent Prints & Forecasts</h3>
<div class="legend">
<span class="key"><span class="sw" style="background:var(--accent)"></span> Actual</span>
<span class="key"><span class="sw" style="background:var(--accent-3)"></span> IMF 2025: 2.9%</span>
<span class="key"><span class="sw" style="background:var(--warn)"></span> ADB 2025: 4.0%</span>
</div>
<div class="chart" id="chart-inflation"></div>
<div class="tools">
<button class="btn ghost export-chart" data-chart="chart-inflation"><i data-feather="image"></i> Save PNG</button>
</div>
</div>
<div class="chart-card">
<h3>Labor Market Snapshot</h3>
<div class="legend">
<span class="key"><span class="sw" style="background:var(--accent-2)"></span> Unemployment</span>
</div>
<div class="chart" id="chart-labor"></div>
<div class="tools">
<button class="btn ghost export-chart" data-chart="chart-labor"><i data-feather="image"></i> Save PNG</button>
</div>
</div>
</div>
</section>
<section class="section with-toolbar" id="fdi" data-title="Foreign Direct Investment">
<div class="toolbar-row">
<h2>Foreign Direct Investment</h2>
<div class="tools">
<button class="btn ghost link-section" data-target="fdi"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<p>FDI inflows are robust, reflecting sustained foreign investor confidence. Registered capital surged while disbursements kept pace, and H1 totals surpassed $21B.</p>
<div class="grid cols-2">
<div class="chart-card">
<h3>FDI Momentum</h3>
<div class="legend">
<span class="key"><span class="sw" style="background:var(--accent)"></span> Registered (Jan–May)</span>
<span class="key"><span class="sw" style="background:var(--accent-3)"></span> Disbursed (Jan–May)</span>
<span class="key"><span class="sw" style="background:var(--accent-2)"></span> Total (H1)</span>
</div>
<div class="chart" id="chart-fdi"></div>
<div class="tools">
<button class="btn ghost export-chart" data-chart="chart-fdi"><i data-feather="image"></i> Save PNG</button>
</div>
</div>
<div class="section" style="margin:0;">
<h3>Investor Takeaways</h3>
<ul>
<li>Strength across diversified manufacturing and services clusters.</li>
<li>Policy continuity and infrastructure investments support pipeline.</li>
<li>Monitor sectoral concentration and supply-chain dependencies.</li>
</ul>
<div class="info">Policy note: Maintain macro buffers; prioritize quality FDI with technology transfer and value chain deepening.</div>
</div>
</div>
</section>
<section class="section with-toolbar" id="sectoral" data-title="Sectoral Analysis">
<div class="toolbar-row">
<h2>Sectoral Analysis</h2>
<div class="tools">
<button class="btn ghost link-section" data-target="sectoral"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<div class="chips" id="sectorFilters">
<label class="chip"><input type="checkbox" data-sector="services" checked> Services</label>
<label class="chip"><input type="checkbox" data-sector="manufacturing" checked> Manufacturing</label>
<label class="chip"><input type="checkbox" data-sector="export" checked> Export Industries</label>
<label class="chip"><input type="checkbox" data-sector="banking" checked> Banking</label>
</div>
<div class="grid cols-2 cols-3-xl" id="sectorCards">
<div class="chart-card sector services">
<h3>Services</h3>
<p>Primary growth engine with broad-based expansion across retail, logistics, tourism, and digital services.</p>
</div>
<div class="chart-card sector manufacturing">
<h3>Manufacturing</h3>
<p>Recovery maintained with strong export orders; supply-chain realignment favors Vietnam’s cost-quality mix.</p>
</div>
<div class="chart-card sector export">
<h3>Export Industries</h3>
<p>Remain the economic backbone; competitiveness intact despite tariff-related frictions.</p>
</div>
<div class="chart-card sector banking">
<h3>Banking</h3>
<p>Earnings projected +17% in 2025 on ~15% system-wide credit growth; watch asset quality under external stress.</p>
</div>
</div>
</section>
<section class="section with-toolbar" id="risks" data-title="Challenges and Risks">
<div class="toolbar-row">
<h2>Challenges and Risk Factors</h2>
<div class="tools">
<button class="btn ghost link-section" data-target="risks"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<ul>
<li>Global trade tensions and US tariff policies pressure export-oriented sectors.</li>
<li>Geopolitical instability raises uncertainty and potential cost pass-through.</li>
<li>Overreliance on FDI could amplify cyclicality and external shocks.</li>
<li>Imperative: growth not at the cost of macro stability, public debt, or inflation.</li>
</ul>
<div class="notice"><strong>Mitigation:</strong> diversify export markets, strengthen domestic demand, enhance resilience, and maintain fiscal space for counter-cyclical support.</div>
</section>
<section class="section with-toolbar" id="history" data-title="Historical Comparison">
<div class="toolbar-row">
<h2>Historical Comparison</h2>
<div class="tools">
<button class="btn ghost link-section" data-target="history"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<p>Vietnam sustained high growth in 2024 (7.1%). Growth in 2025 may moderate versus ambitious targets due to external constraints, but fundamentals remain resilient.</p>
<div class="chart-card">
<h3>Q1 GDP Growth 2020–2025 (y/y)</h3>
<div class="legend">
<span class="key"><span class="sw" style="background:var(--accent)"></span> Q1 Growth</span>
<span class="key"><span class="sw" style="background:var(--accent-2)"></span> Trend</span>
</div>
<div class="chart" id="chart-history"></div>
<div class="tools">
<button class="btn ghost export-chart" data-chart="chart-history"><i data-feather="image"></i> Save PNG</button>
</div>
</div>
</section>
<section class="section with-toolbar" id="outlook" data-title="Economic Outlook">
<div class="toolbar-row">
<h2>Economic Outlook and Projections</h2>
<div class="tools">
<button class="btn ghost link-section" data-target="outlook"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<div class="grid cols-2">
<div>
<h3>Near-term Prospects (2025)</h3>
<p>Solid starting point in Q1 (6.9% y/y) with resilience expected despite uncertainty. Government’s 8.3–8.5% target is ambitious vs international forecasts, but domestic drivers provide ballast.</p>
<h4>Key Supporting Factors</h4>
<ul>
<li>Robust FDI inflows and investor confidence</li>
<li>Low unemployment supporting consumption</li>
<li>Controlled inflation sustaining purchasing power</li>
<li>Export competitiveness and market diversification</li>
<li>Parliamentary push to raise GDP growth to at least 8%</li>
</ul>
</div>
<div>
<h3>Policy Playbook</h3>
<ul>
<li>Diversify export markets and deepen regional value chains.</li>
<li>Strengthen domestic demand through targeted measures.</li>
<li>Enhance resilience: buffers, liquidity backstops, prudent credit growth.</li>
<li>Use fiscal space counter-cyclically if global shocks intensify.</li>
</ul>
<div class="info">Bottom line: Cautiously optimistic baseline with upside from supply-chain shifts; vigilance on external risks is warranted.</div>
</div>
</div>
</section>
<section class="section with-toolbar" id="conclusion" data-title="Conclusion">
<div class="toolbar-row">
<h2>Conclusion</h2>
<div class="tools">
<button class="btn ghost link-section" data-target="conclusion"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<p>Vietnam’s 2025 performance highlights resilience and strong fundamentals: low unemployment, contained inflation, and buoyant FDI. While international projections are more conservative than the government’s 8%+ target, momentum and reform commitment provide a solid backdrop for sustained development.</p>
</section>
<section class="section with-toolbar" id="methodology" data-title="Methodology">
<div class="toolbar-row">
<h2>Research Methodology</h2>
<div class="tools">
<button class="btn ghost link-section" data-target="methodology"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<details class="collapsible">
<summary>Scope, Sources, and Processing <i data-feather="chevron-down"></i></summary>
<div class="content">
<ul>
<li>Scope: Macroeconomic indicators for Vietnam 2024–2025 with emphasis on Q1/Q2 2025 and H1 aggregates.</li>
<li>Sources: IMF, ADB, World Bank, GSO Vietnam, Trading Economics, Vietnam Investment Review, and related portals.</li>
<li>Processing: Extracted key values, normalized units, and constructed trend series for visualization. Forecasts aggregated for cross-comparison.</li>
<li>Validation: Cross-checked ranges and narratives against cited sources; flagged assumptions where applicable.</li>
</ul>
</div>
</details>
</section>
<section class="section with-toolbar" id="references" data-title="Sources & Citations">
<div class="toolbar-row">
<h2>Sources and Citations</h2>
<div class="tools">
<button class="btn ghost link-section" data-target="references"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<div id="citationList" class="grid">
<div class="table-wrap">
<table>
<thead>
<tr><th>#</th><th>Source</th><th>URL</th><th>Actions</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>Trading Economics - Vietnam GDP Annual Growth Rate</td><td><a href="https://tradingeconomics.com/vietnam/gdp-growth-annual" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Trading Economics - Vietnam GDP Annual Growth Rate: https://tradingeconomics.com/vietnam/gdp-growth-annual"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>2</td><td>International Monetary Fund - Vietnam Country Profile</td><td><a href="https://www.imf.org/en/Countries/VNM" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="IMF - Vietnam Country Profile: https://www.imf.org/en/Countries/VNM"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>3</td><td>World Economics - Vietnam GDP Estimates</td><td><a href="https://www.worldeconomics.com/GDP/Vietnam.gdp" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="World Economics - Vietnam GDP Estimates: https://www.worldeconomics.com/GDP/Vietnam.gdp"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>4</td><td>General Statistics Office (Vietnam)</td><td><a href="https://www.gso.gov.vn/en/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="General Statistics Office (Vietnam): https://www.gso.gov.vn/en/"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>5</td><td>Wikipedia - Economy of Vietnam</td><td><a href="https://en.wikipedia.org/wiki/Economy_of_Vietnam" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Wikipedia - Economy of Vietnam: https://en.wikipedia.org/wiki/Economy_of_Vietnam"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>6</td><td>IMF - Vietnam and the IMF</td><td><a href="https://www.imf.org/en/Countries/VNM" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="IMF - Vietnam and the IMF: https://www.imf.org/en/Countries/VNM"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>7</td><td>FocusEconomics - Vietnam Economic Indicators</td><td><a href="https://www.focus-economics.com/countries/vietnam" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="FocusEconomics - Vietnam Economic Indicators: https://www.focus-economics.com/countries/vietnam"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>8</td><td>GSO Vietnam - Data & Statistics</td><td><a href="https://www.gso.gov.vn/en/data-and-statistics/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="GSO Vietnam - Data & Statistics: https://www.gso.gov.vn/en/data-and-statistics/"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>9</td><td>VietnamNet - Economic News</td><td><a href="https://vietnamnet.vn/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="VietnamNet - Economic News: https://vietnamnet.vn/"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>10</td><td>IMF - Article IV Mission Reports</td><td><a href="https://www.imf.org/en/Publications/CR" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="IMF - Article IV Mission Reports: https://www.imf.org/en/Publications/CR"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>11</td><td>Vietnam Briefing - Analysis</td><td><a href="https://www.vietnam-briefing.com/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Vietnam Briefing - Economic Analysis: https://www.vietnam-briefing.com/"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>12</td><td>Vietnam Investment Review - FDI</td><td><a href="https://vir.com.vn/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Vietnam Investment Review - FDI Statistics: https://vir.com.vn/"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>13</td><td>Trading Economics - FDI</td><td><a href="https://tradingeconomics.com/vietnam/foreign-direct-investment" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Trading Economics - Vietnam FDI: https://tradingeconomics.com/vietnam/foreign-direct-investment"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>14</td><td>White & Case - Regional Outlook</td><td><a href="https://www.whitecase.com/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="White & Case - Regional Economic Outlook: https://www.whitecase.com/"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>15</td><td>Vietnam Economic Times</td><td><a href="https://vneconomictimes.com/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Vietnam Economic Times: https://vneconomictimes.com/"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>16</td><td>Asian Development Bank - Viet Nam</td><td><a href="https://www.adb.org/countries/viet-nam/main" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="ADB - Viet Nam Country Partnership: https://www.adb.org/countries/viet-nam/main"><i data-feather="copy"></i> Copy</button></td></tr>
<tr><td>17</td><td>Ministry of Planning and Investment</td><td><a href="https://www.mpi.gov.vn/en/" target="_blank" rel="noopener">Link</a></td><td><button class="btn ghost copy-cite" data-cite="Ministry of Planning and Investment (Vietnam): https://www.mpi.gov.vn/en/"><i data-feather="copy"></i> Copy</button></td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section class="section with-toolbar" id="appendix" data-title="Appendices">
<div class="toolbar-row">
<h2>Appendices</h2>
<div class="tools">
<button class="btn ghost link-section" data-target="appendix"><i data-feather="link-2"></i> Link</button>
</div>
</div>
<details class="collapsible">
<summary>Data Dictionary <i data-feather="chevron-down"></i></summary>
<div class="content">
<ul>
<li>GDP Growth: Real GDP y/y change, quarterly and half-year aggregate.</li>
<li>Inflation: Headline CPI y/y.</li>
<li>Unemployment: National unemployment rate.</li>
<li>FDI: Registered and disbursed capital in USD; total H1 in USD.</li>
</ul>
</div>
</details>
<details class="collapsible" style="margin-top:.75rem;">
<summary>Assumptions & Notes <i data-feather="chevron-down"></i></summary>
<div class="content">
<ul>
<li>Q1 historical series reflects reported year-on-year expansions 2020–2025.</li>
<li>Forecast comparison normalizes to calendar 2025 growth.</li>
</ul>
</div>
</details>
</section>
<footer class="foot">
<div class="meta-row">
<span class="pill"><i data-feather="shield"></i> Stable macro</span>
<span class="pill"><i data-feather="cpu"></i> Manufacturing hub</span>
<span class="pill"><i data-feather="trending-up"></i> FDI momentum</span>
</div>
<div class="small">© 2025 Research Dashboard. Built with vanilla Web APIs. Use the Export button to print to PDF or save charts as PNG.</div>
</footer>
<div class="sticky-tools">
<div class="floater">
<button class="btn" id="backToTop"><i data-feather="arrow-up"></i> Top</button>
<button class="btn" id="nextSection"><i data-feather="chevron-down"></i> Next</button>
</div>
</div>
</div>
</div>
</main>
<script>
feather.replace();
// State & Data
const state = {
theme: localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'),
search: { index: [], matches: [], current: -1 },
charts: {}
};
document.documentElement.setAttribute('data-theme', state.theme);
// Datasets
const data = {
gdpBars: [
{label: 'Q1 2025', value: 6.9},
{label: 'Q2 2025', value: 7.96},
{label: 'H1 2025', value: 7.52}
],
forecast: [
{name: 'World Bank', value: 5.8, type: 'intl'},
{name: 'ADB', value: 6.6, type: 'intl'},
{name: 'IMF', value: 5.2, type: 'intl'},
{name: 'Gov Target Lower', value: 8.3, type: 'gov'},
{name: 'Gov Target Upper', value: 8.5, type: 'gov'}
],
inflation: [
{month: 'May 2025', value: 3.24},
{month: 'June 2025', value: 3.57}
],
inflationForecasts: [
{name: 'IMF 2025', value: 2.9, color: 'var(--accent-3)'},
{name: 'ADB 2025', value: 4.0, color: 'var(--warn)'}
],
unemployment: [
{label: 'Q4 2024', value: 2.22},
{label: 'Q1 2025', value: 2.20}
],
fdi: [
{label: 'Registered (Jan–May)', value: 18.4, color: 'var(--accent)'},
{label: 'Disbursed (Jan–May)', value: 8.9, color: 'var(--accent-3)'},
{label: 'Total (H1)', value: 21.51, color: 'var(--accent-2)'}
],
historyQ1: [
{year: 2020, value: 3.21},
{year: 2021, value: 4.85},
{year: 2022, value: 5.42},
{year: 2023, value: 3.46},
{year: 2024, value: 5.98},
{year: 2025, value: 6.93}
]
};
// Utility: formatters
const fmt = {
pct: v => `${(Math.round(v * 100) / 100).toFixed(2)}%`,
moneyB: v => `$${v.toFixed(2)}B`
};
// Theme toggle
document.getElementById('themeToggle').addEventListener('click', () => {
state.theme = (state.theme === 'light') ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', state.theme);
localStorage.setItem('theme', state.theme);
});
// Print/Export
document.getElementById('printBtn').addEventListener('click', () => {
window.print();
});
// Build TOC
const sections = Array.from(document.querySelectorAll('.section, .hero')).map(el => {
if (!el.id) el.id = 'section-' + Math.random().toString(36).slice(2,7);
return el;
});
const tocLinks = document.getElementById('tocLinks');
const tocFrag = document.createDocumentFragment();
sections.forEach(sec => {
const title = sec.dataset.title || sec.querySelector('h1,h2')?.textContent?.trim() || 'Section';
const a = document.createElement('a');
a.href = `#${sec.id}`;
a.textContent = title;
tocFrag.appendChild(a);
});
tocLinks.appendChild(tocFrag);
// IntersectionObserver: active section & progress
const progress = document.getElementById('progressbar');
const linkMap = new Map(Array.from(tocLinks.querySelectorAll('a')).map(a => [a.getAttribute('href').slice(1), a]));
const io = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
linkMap.forEach(a => a.classList.remove('active'));
const a = linkMap.get(entry.target.id);
if (a) a.classList.add('active');
}
});
const viewport = window.scrollY + window.innerHeight;
const doc = document.body.scrollHeight;
const pct = Math.min(100, Math.max(0, (viewport / doc) * 100));
progress.style.width = pct + '%';
}, {rootMargin: '-40% 0px -55% 0px', threshold: [0, 0.25, 0.5, 1]});
sections.forEach(sec => io.observe(sec));
// Smooth link buttons
document.querySelectorAll('.link-section').forEach(btn => btn.addEventListener('click', e => {
const id = e.currentTarget.dataset.target;
location.hash = id;
document.getElementById(id)?.scrollIntoView({behavior: 'smooth', block: 'start'});
}));
document.querySelectorAll('.link-kpi').forEach(btn => btn.addEventListener('click', e => {
const id = e.currentTarget.dataset.link.slice(1);
document.getElementById(id)?.scrollIntoView({behavior: 'smooth', block: 'start'});
}));
// Copy KPI buttons
const copyText = async (txt) => {
try {
await navigator.clipboard.writeText(txt);
toast('Copied to clipboard');
} catch {
const ta = document.createElement('textarea'); ta.value = txt; document.body.appendChild(ta);
ta.select(); document.execCommand('copy'); ta.remove(); toast('Copied');
}
};
document.querySelectorAll('.copy-kpi').forEach(btn => btn.addEventListener('click', e => {
copyText(e.currentTarget.dataset.copy);
}));
document.querySelectorAll('.copy-section').forEach(btn => btn.addEventListener('click', e => {
const id = e.currentTarget.dataset.target;
const el = document.getElementById(id);
copyText(el.innerText.trim());
}));
document.querySelectorAll('.copy-cite').forEach(btn => btn.addEventListener('click', e => {
copyText(e.currentTarget.dataset.cite);
}));
// Back to top & Next section
document.getElementById('backToTop').addEventListener('click', () => window.scrollTo({top:0, behavior:'smooth'}));
document.getElementById('nextSection').addEventListener('click', () => {
const y = window.scrollY;
const next = sections.find(sec => sec.getBoundingClientRect().top + window.scrollY > y + 20);
next?.scrollIntoView({behavior: 'smooth', block: 'start'});
});
// Search with highlight and navigation
const searchInput = document.getElementById('globalSearch');
const clearSearch = () => {
document.querySelectorAll('.highlight').forEach(n => {
const parent = n.parentNode; parent.replaceChild(document.createTextNode(n.textContent), n); parent.normalize();
});
state.search.matches = []; state.search.current = -1;
};
document.getElementById('clearSearch').addEventListener('click', () => {
searchInput.value = ''; clearSearch();
});
const highlightAll = (term) => {
clearSearch();
if (!term) return;
const walker = document.createTreeWalker(document.querySelector('main'), NodeFilter.SHOW_TEXT, {
acceptNode: node => {
const t = node.nodeValue.trim();
if (!t || node.parentElement.closest('script,style,svg,code,pre,button,nav,header,footer')) return NodeFilter.FILTER_REJECT;
return t.toLowerCase().includes(term.toLowerCase()) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
});
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
nodes.forEach(textNode => {
const val = textNode.nodeValue;
const idx = val.toLowerCase().indexOf(term.toLowerCase());
if (idx > -1) {
const span = document.createElement('span');
span.className = 'highlight';
span.textContent = val.substr(idx, term.length);
const before = document.createTextNode(val.substr(0, idx));
const after = document.createTextNode(val.substr(idx + term.length));
const parent = textNode.parentNode;
parent.replaceChild(after, textNode);
parent.insertBefore(span, after);
parent.insertBefore(before, span);
}
});
state.search.matches = Array.from(document.querySelectorAll('.highlight'));
state.search.current = -1;
if (state.search.matches.length) gotoMatch(0);
};
const gotoMatch = (i) => {
if (!state.search.matches.length) return;
state.search.current = (i + state.search.matches.length) % state.search.matches.length;
const el = state.search.matches[state.search.current];
el.scrollIntoView({behavior:'smooth', block:'center'});
el.animate([{background: 'var(--mark)'}, {background: 'transparent'}], {duration: 1500, fill: 'forwards'});
};
searchInput.addEventListener('input', e => highlightAll(e.target.value.trim()));
window.addEventListener('keydown', (e) => {
if (e.key === '/' && document.activeElement !== searchInput) {
e.preventDefault(); searchInput.focus(); searchInput.select();
} else if ((e.key === 'Enter' || e.key === 'ArrowDown') && state.search.matches.length) {
gotoMatch(state.search.current + 1);
} else if (e.key === 'ArrowUp' && state.search.matches.length) {
gotoMatch(state.search.current - 1);
}
});
// Sortable tables
const sortTable = (table, colIndex, type, asc) => {
const tbody = table.tBodies[0];
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a,b) => {
const av = a.children[colIndex].dataset.v ?? a.children[colIndex].textContent;
const bv = b.children[colIndex].dataset.v ?? b.children[colIndex].textContent;
if (type === 'num') return (parseFloat(av) - parseFloat(bv)) * (asc ? 1 : -1);
return av.toString().localeCompare(bv.toString()) * (asc ? 1 : -1);
});
rows.forEach(r => tbody.appendChild(r));
};
document.querySelectorAll('table thead th').forEach((th, i) => {
let asc = true;
th.addEventListener('click', () => {
sortTable(th.closest('table'), i, th.dataset.sort, asc);
asc = !asc;
});
});
// Export data CSV
document.getElementById('downloadData').addEventListener('click', () => {
const rows = [
['Indicator','Period','Value','Notes'],
['GDP Growth','Q1 2025','6.9%','y/y'],
['GDP Growth','Q2 2025','7.96%','y/y'],
['GDP Growth','H1 2025','7.52%','Highest since 2011'],
['Inflation','May 2025','3.24%','YTD'],
['Inflation','June 2025','3.57%','YTD high'],
['Unemployment','Q1 2025','2.20%','Down from 2.22%'],
['FDI Registered','Jan–May 2025','$18.4B','+51% y/y'],
['FDI Disbursed','Jan–May 2025','$8.9B',''],
['FDI Total','H1 2025','$21.51B','+32.6% y/y']
];
const csv = rows.map(r => r.map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n');
const blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = 'vietnam_2025_indicators.csv';
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
});
// Sector filters
const sectorFilters = document.getElementById('sectorFilters');
sectorFilters.addEventListener('change', () => {
const active = new Set(Array.from(sectorFilters.querySelectorAll('input:checked')).map(i => i.dataset.sector));
document.querySelectorAll('#sectorCards .sector').forEach(card => {
const cat = [...card.classList].find(c => ['services','manufacturing','export','banking'].includes(c));
card.style.display = active.has(cat) ? '' : 'none';
});
localStorage.setItem('sectorFilters', JSON.stringify([...active]));
});
// Restore sector filters
const savedFilters = JSON.parse(localStorage.getItem('sectorFilters') || 'null');
if (savedFilters) {
sectorFilters.querySelectorAll('input').forEach(i => i.checked = savedFilters.includes(i.dataset.sector));
sectorFilters.dispatchEvent(new Event('change'));
}
// Small toasts
let toastTimer;
function toast(msg) {
clearTimeout(toastTimer);
let el = document.getElementById('toast');
if (!el) {
el = document.createElement('div');
el.id = 'toast';
el.style.position = 'fixed';
el.style.bottom = '18px';
el.style.left = '50%';
el.style.transform = 'translateX(-50%)';
el.style.background = 'var(--panel)';
el.style.border = '1px solid var(--bdr)';
el.style.boxShadow = 'var(--shadow)';
el.style.color = 'var(--text)';
el.style.padding = '.6rem .9rem';
el.style.borderRadius = '10px';
el.style.zIndex = '999';
document.body.appendChild(el);
}
el.textContent = msg;
el.style.opacity = '1';
toastTimer = setTimeout(() => { el.style.transition = 'opacity .4s'; el.style.opacity = '0'; }, 1500);
}
// Charts: SVG helpers
function createSVG(w, h) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
svg.setAttribute('width', '100%'); svg.setAttribute('height', '100%');
return svg;
}
function axis(svg, x, y, w, h, ticks=5, max=10, format=v=>v.toString()) {
const g = document.createElementNS(svg.namespaceURI, 'g');
g.setAttribute('stroke', 'currentColor');
g.setAttribute('opacity', '.5');
for (let i=0;i<=ticks;i++) {
const ty = y + h - (i/ticks)*h;
const line = document.createElementNS(svg.namespaceURI, 'line');
line.setAttribute('x1', x); line.setAttribute('x2', x+w);
line.setAttribute('y1', ty); line.setAttribute('y2', ty);
line.setAttribute('stroke-width', i===0?1.2:0.6);
g.appendChild(line);
const label = document.createElementNS(svg.namespaceURI, 'text');
label.setAttribute('x', x - 6);
label.setAttribute('y', ty + 4);
label.setAttribute('font-size', '12');
label.setAttribute('text-anchor', 'end');
label.textContent = format((i/ticks)*max);
g.appendChild(label);
}
svg.appendChild(g);
}
function exportSVGToPNG(svgEl, filename='chart.png') {
const svgData = new XMLSerializer().serializeToString(svgEl);
const canvas = document.createElement('canvas');
const bbox = svgEl.viewBox.baseVal;
canvas.width = bbox.width * 2;
canvas.height = bbox.height * 2;
const ctx = canvas.getContext('2d');
const img = new Image();
const svgBlob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(svgBlob);
img.onload = function() {
ctx.scale(2,2);
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
canvas.toBlob(function(blob) {
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
a.download = filename; document.body.appendChild(a); a.click(); a.remove();
});
};
img.src = url;
}
// Chart: Bars with optional target band
function drawBars(containerId, series, opts = {}) {
const el = document.getElementById(containerId);
el.innerHTML = '';
const svg = createSVG(800, 480);
el.appendChild(svg);
const pad = {l: 60, r: 20, t: 30, b: 60};
const w = 800 - pad.l - pad.r;
const h = 480 - pad.t - pad.b;
const max = Math.max(opts.max || 10, ...series.map(d => d.value)) + 0.5;
// Target band
if (opts.targetBand?.length && document.getElementById('toggleTargetBand')?.checked) {
const [lo, hi] = opts.targetBand;
const yLo = pad.t + h - (lo / max) * h;
const yHi = pad.t + h - (hi / max) * h;
const rect = document.createElementNS(svg.namespaceURI, 'rect');
rect.setAttribute('x', pad.l);
rect.setAttribute('width', w);
rect.setAttribute('y', yHi);
rect.setAttribute('height', Math.max(2, yLo - yHi));
rect.setAttribute('fill', 'url(#bandGrad)');
rect.setAttribute('opacity', '0.35');
// gradient
const defs = document.createElementNS(svg.namespaceURI, 'defs');
const grad = document.createElementNS(svg.namespaceURI, 'linearGradient');
grad.setAttribute('id','bandGrad'); grad.setAttribute('x1','0%'); grad.setAttribute('x2','0%'); grad.setAttribute('y1','0%'); grad.setAttribute('y2','100%');
const s1 = document.createElementNS(svg.namespaceURI, 'stop'); s1.setAttribute('offset','0%'); s1.setAttribute('stop-color','var(--warn)');
const s2 = document.createElementNS(svg.namespaceURI, 'stop'); s2.setAttribute('offset','100%'); s2.setAttribute('stop-color','var(--accent-2)');
grad.appendChild(s1); grad.appendChild(s2); defs.appendChild(grad); svg.appendChild(defs);
svg.appendChild(rect);
}
axis(svg, pad.l, pad.t, w, h, 5, max, v => (v).toFixed(0) + '%');
const bw = w / (series.length * 1.5);
series.forEach((d, i) => {
const x = pad.l + i * (w / series.length) + (w / series.length - bw) / 2;
const y = pad.t + h - (d.value / max) * h;
const rect = document.createElementNS(svg.namespaceURI, 'rect');
rect.setAttribute('x', x);
rect.setAttribute('y', y);
rect.setAttribute('width', bw);
rect.setAttribute('height', Math.max(2, pad.t + h - y));
rect.setAttribute('rx', '6');
rect.setAttribute('fill', 'var(--accent)');
rect.setAttribute('opacity', '.9');
svg.appendChild(rect);
const lbl = document.createElementNS(svg.namespaceURI, 'text');
lbl.setAttribute('x', x + bw/2);
lbl.setAttribute('y', pad.t + h + 20);
lbl.setAttribute('text-anchor', 'middle');
lbl.setAttribute('font-size', '12');
lbl.textContent = d.label;
svg.appendChild(lbl);
const val = document.createElementNS(svg.namespaceURI, 'text');
val.setAttribute('x', x + bw/2);
val.setAttribute('y', y - 6);
val.setAttribute('text-anchor', 'middle');
val.setAttribute('font-size', '12');
val.textContent = d.value.toFixed(2) + '%';
svg.appendChild(val);
});
state.charts[containerId] = svg;
}
// Chart: Dot plot (forecasts)
function drawDotPlot(containerId, series, opts={}) {
const el = document.getElementById(containerId); el.innerHTML = '';
const svg = createSVG(800, 480); el.appendChild(svg);
const pad = {l: 70, r: 20, t: 20, b: 60};
const w = 800 - pad.l - pad.r, h = 480 - pad.t - pad.b;
const max = Math.max(opts.max || 9, ...series.map(d => d.value));
const min = opts.min || 0;
const filtered = series.filter(d => d.value >= (opts.threshold ?? 0));
// axis
axis(svg, pad.l, pad.t, w, h, 6, max, v => (v).toFixed(0) + '%');
// baseline 0
const x0 = pad.l, yMid = pad.t + h;
// x scale
const xScale = v => pad.l + (v - min) / (max - min) * w;
// categories y positions
const items = filtered;
const gap = h / (items.length + 1);
items.forEach((d, i) => {
const y = pad.t + gap * (i+1);
const x = xScale(d.value);
const line = document.createElementNS(svg.namespaceURI, 'line');
line.setAttribute('x1', pad.l); line.setAttribute('x2', pad.l + w); line.setAttribute('y1', y); line.setAttribute('y2', y);
line.setAttribute('stroke', 'currentColor'); line.setAttribute('opacity', '.08');
svg.appendChild(line);
const dot = document.createElementNS(svg.namespaceURI, 'circle');
dot.setAttribute('cx', x); dot.setAttribute('cy', y); dot.setAttribute('r', '8');
dot.setAttribute('fill', d.type === 'gov' ? 'var(--accent-2)' : 'var(--accent-3)');
dot.setAttribute('opacity', '.9');
svg.appendChild(dot);
const label = document.createElementNS(svg.namespaceURI, 'text');
label.setAttribute('x', pad.l - 10); label.setAttribute('y', y + 4);
label.setAttribute('text-anchor', 'end'); label.setAttribute('font-size', '12'); label.textContent = d.name;
svg.appendChild(label);
const val = document.createElementNS(svg.namespaceURI, 'text');
val.setAttribute('x', x + 10); val.setAttribute('y', y + 4); val.setAttribute('font-size','12'); val.textContent = d.value.toFixed(1) + '%';
svg.appendChild(val);
});
state.charts[containerId] = svg;
}
// Chart: Line
function drawLine(containerId, series, opts={}) {
const el = document.getElementById(containerId); el.innerHTML = '';
const svg = createSVG(800, 480); el.appendChild(svg);
const pad = {l: 60, r: 20, t: 20, b: 60};
const w = 800 - pad.l - pad.r, h = 480 - pad.t - pad.b;
const max = Math.max(opts.max || Math.max(...series.map(d=>d.value)) + 1, ...series.map(d => d.value));
const min = Math.min(opts.min || Math.min(...series.map(d=>d.value)) - 0.5, ...series.map(d => d.value));
axis(svg, pad.l, pad.t, w, h, 6, max, v => (v).toFixed(0) + '%');
const xScale = (i) => pad.l + (i / (series.length - 1)) * w;
const yScale = (v) => pad.t + h - ((v - 0) / (max - 0)) * h;
const path = document.createElementNS(svg.namespaceURI, 'path');
const d = series.map((p, i) => `${i===0?'M':'L'} ${xScale(i)} ${yScale(p.value)}`).join(' ');
path.setAttribute('d', d);
path.setAttribute('fill', 'none'); path.setAttribute('stroke', 'var(--accent)'); path.setAttribute('stroke-width', '3');
svg.appendChild(path);
// points
series.forEach((p, i) => {
const c = document.createElementNS(svg.namespaceURI, 'circle');
c.setAttribute('cx', xScale(i)); c.setAttribute('cy', yScale(p.value)); c.setAttribute('r', '5');
c.setAttribute('fill', 'var(--accent)'); svg.appendChild(c);
const label = document.createElementNS(svg.namespaceURI, 'text');
label.setAttribute('x', xScale(i)); label.setAttribute('y', pad.t + h + 20);
label.setAttribute('text-anchor', 'middle'); label.setAttribute('font-size','12'); label.textContent = p.year;
svg.appendChild(label);
const val = document.createElementNS(svg.namespaceURI, 'text');
val.setAttribute('x', xScale(i)); val.setAttribute('y', yScale(p.value) - 8);
val.setAttribute('text-anchor','middle'); val.setAttribute('font-size','12'); val.textContent = p.value.toFixed(2) + '%';
svg.appendChild(val);
});
// trendline
const n = series.length;
const meanX = (n - 1) / 2;
const meanY = series.reduce((a, b) => a + b.value, 0) / n;
let num = 0, den = 0;
series.forEach((p, i) => { num += (i - meanX) * (p.value - meanY); den += (i - meanX) ** 2; });
const slope = num / den; const intercept = meanY - slope * meanX;
const y1 = intercept; const y2 = slope * (n-1) + intercept;
const line = document.createElementNS(svg.namespaceURI, 'line');
line.setAttribute('x1', xScale(0)); line.setAttribute('x2', xScale(n-1));
line.setAttribute('y1', yScale(y1)); line.setAttribute('y2', yScale(y2));
line.setAttribute('stroke', 'var(--accent-2)'); line.setAttribute('stroke-width', '2'); line.setAttribute('stroke-dasharray','6,6');
svg.appendChild(line);
state.charts[containerId] = svg;
}
// Chart: Mixed bars (FDI) and labels
function drawFDI(containerId, series) {
const el = document.getElementById(containerId); el.innerHTML = '';
const svg = createSVG(800, 480); el.appendChild(svg);
const pad = {l: 70, r: 20, t: 30, b: 80};
const w = 800 - pad.l - pad.r, h = 480 - pad.t - pad.b;
const max = Math.max(...series.map(d => d.value)) + 2;
axis(svg, pad.l, pad.t, w, h, 6, max, v => '$' + v.toFixed(0) + 'B');
const bw = w / (series.length * 1.6);
series.forEach((d, i) => {
const x = pad.l + i * (w / series.length) + (w / series.length - bw) / 2;
const y = pad.t + h - (d.value / max) * h;
const rect = document.createElementNS(svg.namespaceURI, 'rect');
rect.setAttribute('x', x); rect.setAttribute('y', y);
rect.setAttribute('width', bw); rect.setAttribute('height', Math.max(2, pad.t + h - y));
rect.setAttribute('rx','6'); rect.setAttribute('fill', d.color || 'var(--accent)');
svg.appendChild(rect);
const lbl = document.createElementNS(svg.namespaceURI, 'text');
lbl.setAttribute('x', x + bw/2); lbl.setAttribute('y', pad.t + h + 20);
lbl.setAttribute('text-anchor','middle'); lbl.setAttribute('font-size','12'); lbl.textContent = d.label;
svg.appendChild(lbl);
const val = document.createElementNS(svg.namespaceURI, 'text');
val.setAttribute('x', x + bw/2); val.setAttribute('y', y - 6);
val.setAttribute('text-anchor','middle'); val.setAttribute('font-size','12'); val.textContent = fmt.moneyB(d.value);
svg.appendChild(val);
});
state.charts[containerId] = svg;
}
// Chart: Inflation line + forecast markers
function drawInflation(containerId, actual, forecasts) {
const el = document.getElementById(containerId); el.innerHTML = '';
const svg = createSVG(800, 480); el.appendChild(svg);
const pad = {l: 60, r: 20, t: 20, b: 60};
const w = 800 - pad.l - pad.r, h = 480 - pad.t - pad.b;
const allVals = [...actual.map(d=>d.value), ...forecasts.map(f=>f.value)];
const max = Math.max(5, ...allVals) + 0.5;
axis(svg, pad.l, pad.t, w, h, 5, max, v => v.toFixed(0) + '%');
const xScale = (i) => pad.l + (i / (actual.length - 1)) * w;
const yScale = (v) => pad.t + h - (v / max) * h;
const path = document.createElementNS(svg.namespaceURI, 'path');
const d = actual.map((p, i) => `${i===0?'M':'L'} ${xScale(i)} ${yScale(p.value)}`).join(' ');
path.setAttribute('d', d);
path.setAttribute('fill', 'none'); path.setAttribute('stroke', 'var(--accent)'); path.setAttribute('stroke-width','3');
svg.appendChild(path);
actual.forEach((p, i) => {
const c = document.createElementNS(svg.namespaceURI, 'circle');
c.setAttribute('cx', xScale(i)); c.setAttribute('cy', yScale(p.value)); c.setAttribute('r', '6');
c.setAttribute('fill', 'var(--accent)'); svg.appendChild(c);
const label = document.createElementNS(svg.namespaceURI, 'text');
label.setAttribute('x', xScale(i)); label.setAttribute('y', pad.t + h + 20);
label.setAttribute('text-anchor','middle'); label.setAttribute('font-size','12'); label.textContent = p.month;
svg.appendChild(label);
});
// Forecast markers at end
forecasts.forEach((f, idx) => {
const x = pad.l + w - (idx * 80);
const y = yScale(f.value);
const rect = document.createElementNS(svg.namespaceURI, 'rect');
rect.setAttribute('x', x - 40); rect.setAttribute('y', y - 10);
rect.setAttribute('width', 80); rect.setAttribute('height', 20);
rect.setAttribute('rx', '6'); rect.setAttribute('fill', f.color || 'var(--accent-3)'); rect.setAttribute('opacity', '.15');
svg.appendChild(rect);
const line = document.createElementNS(svg.namespaceURI, 'line');
line.setAttribute('x1', x); line.setAttribute('x2', x); line.setAttribute('y1', y); line.setAttribute('y2', yScale(actual.at(-1).value));
line.setAttribute('stroke', f.color || 'var(--accent-3)'); line.setAttribute('stroke-dasharray','4,4');
svg.appendChild(line);
const dot = document.createElementNS(svg.namespaceURI, 'circle');
dot.setAttribute('cx', x); dot.setAttribute('cy', y); dot.setAttribute('r', '6'); dot.setAttribute('fill', f.color || 'var(--accent-3)');
svg.appendChild(dot);
const label = document.createElementNS(svg.namespaceURI, 'text');
label.setAttribute('x', x); label.setAttribute('y', y - 10); label.setAttribute('text-anchor','middle'); label.setAttribute('font-size','12'); label.textContent = f.name + ': ' + f.value.toFixed(1) + '%';
svg.appendChild(label);
});
state.charts[containerId] = svg;
}
// Init charts
const initCharts = () => {
drawBars('chart-gdp-bars', data.gdpBars, {targetBand: [8.3, 8.5]});
drawDotPlot('chart-forecasts', data.forecast, {threshold: parseFloat(document.getElementById('forecastMin').value)});
drawInflation('chart-inflation', data.inflation, data.inflationForecasts);
drawLine('chart-labor', data.unemployment.map((d, i) => ({year: d.label, value: d.value})), {min: 0, max: 5});
drawFDI('chart-fdi', data.fdi);
drawLine('chart-history', data.historyQ1.map(d => ({year: d.year, value: d.value})), {min: 0, max: 9});
};
initCharts();
// Controls for charts
document.getElementById('forecastMin').addEventListener('input', (e) => {
document.getElementById('forecastMinVal').textContent = e.target.value + '%';
drawDotPlot('chart-forecasts', data.forecast, {threshold: parseFloat(e.target.value)});
});
document.getElementById('toggleTargetBand').addEventListener('change', () => {
drawBars('chart-gdp-bars', data.gdpBars, {targetBand: [8.3, 8.5]});
});
document.querySelectorAll('.export-chart').forEach(btn => btn.addEventListener('click', e => {
const id = e.currentTarget.dataset.chart;
const svg = state.charts[id];
if (svg) exportSVGToPNG(svg, id + '.png');
}));
// Clipboard for link to section
document.querySelectorAll('.link-section').forEach(btn => btn.addEventListener('click', (e) => {
const id = e.currentTarget.dataset.target;
const url = location.origin + location.pathname + '#' + id;
navigator.clipboard.writeText(url).then(() => toast('Section link copied'));
}));
// KPI copy accessible via Enter
document.querySelectorAll('.copy-kpi').forEach(btn => btn.addEventListener('keydown', (e) => {
if (e.key === 'Enter') btn.click();
}));
// Re-render charts on theme change to ensure color variables apply (SVG inline uses CSS vars)
const observerTheme = new MutationObserver(() => {
Object.values(state.charts).forEach(svg => {}); // no-op; CSS vars update automatically
});
observerTheme.observe(document.documentElement, {attributes: true, attributeFilter: ['data-theme']});
// Responsive redrawing on container resize
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => initCharts(), 150);
});
// Feather icons in dynamically created toasts will not need replacement, but ensure all are set at load.
</script>
</body>
</html>