gpt5-v3 / index.html
thucdangvan020999's picture
Upload index.html with huggingface_hub
2a92f8a verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vietnam Economic Growth Report 2025 — Interactive Research Presentation</title>
<meta name="description" content="An interactive, single-file research website summarizing Vietnam’s 2025 economic performance with charts, sortable tables, navigation, and citations." />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/fonts/remixicon.css" rel="stylesheet">
<style>
:root {
--bg: #0f1220;
--surface: #151935;
--surface-2: #1b2148;
--text: #e7ecff;
--text-dim: #b8c0e0;
--brand: #5dd5ff;
--brand-2: #8a7bff;
--accent: #38e7aa;
--danger: #ff6b6b;
--warn: #ffcc66;
--ok: #69db7c;
--muted: #2a315f;
--outline: color-mix(in oklab, var(--text) 15%, transparent);
--radius: 14px;
--shadow: 0 10px 30px rgba(0,0,0,.35);
--gap: clamp(12px, 1.8vw, 20px);
--fs-1: clamp(28px, 4.2vw, 52px);
--fs-2: clamp(22px, 3.2vw, 36px);
--fs-3: clamp(18px, 2.2vw, 22px);
--fs-4: clamp(14px, 1.6vw, 16px);
--nav-w: 280px;
color-scheme: dark;
}
body.theme-light {
--bg: #f6f8ff;
--surface: #ffffff;
--surface-2: #f1f3ff;
--text: #1b2559;
--text-dim: #3c4770;
--brand: #005ae0;
--brand-2: #7c4dff;
--accent: #00b894;
--muted: #dfe6ff;
--outline: rgba(0,0,0,.08);
color-scheme: light;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background: radial-gradient(1500px 800px at 80% -10%, color-mix(in oklab, var(--brand) 12%, transparent) 0, transparent 60%), var(--bg);
color: var(--text);
line-height: 1.55;
}
a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }
.app {
display: grid;
grid-template-rows: auto 1fr;
min-height: 100dvh;
}
header {
position: sticky; top: 0; z-index: 50;
backdrop-filter: blur(10px);
background: color-mix(in oklab, var(--surface) 80%, transparent);
border-bottom: 1px solid var(--outline);
}
.topbar {
display: grid;
grid-template-columns: 1fr auto auto;
gap: var(--gap);
max-width: 1400px;
margin: 0 auto;
padding: 10px clamp(12px, 3vw, 28px);
align-items: center;
}
.brand {
display: flex; align-items: center; gap: 12px;
font-weight: 800; letter-spacing: .3px;
font-size: clamp(16px, 2vw, 20px);
}
.brand .logo {
width: 38px; height: 38px; display: grid; place-items: center;
border-radius: 12px;
background: linear-gradient(145deg, var(--brand), var(--brand-2));
color: white; box-shadow: var(--shadow);
}
.controls { display: flex; align-items: center; gap: 10px; }
.search {
display: flex; align-items: center; gap: 8px; width: min(680px, 100%);
background: var(--surface-2); border: 1px solid var(--outline);
border-radius: 12px; padding: 8px 12px;
}
.search input {
border: 0; outline: 0; width: 100%;
background: transparent; color: var(--text);
font-size: var(--fs-4);
}
.btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 8px 12px; border-radius: 12px;
border: 1px solid var(--outline); background: var(--surface);
color: var(--text); cursor: pointer;
}
.btn:hover { background: color-mix(in oklab, var(--surface) 85%, var(--brand) 5%); }
.btn.primary { background: linear-gradient(135deg, var(--brand), var(--brand-2)); border-color: transparent; color: white; }
.btn.ghost { background: transparent; }
.icon { font-size: 18px; }
.progress {
height: 3px; width: 100%; background: linear-gradient(90deg, var(--brand), var(--accent));
transform-origin: left; transform: scaleX(0);
}
.layout {
display: grid;
grid-template-columns: 1fr;
gap: var(--gap);
max-width: 1400px;
padding: clamp(14px, 3vw, 28px);
margin: 0 auto;
}
.hero {
display: grid; gap: var(--gap);
grid-template-columns: 1fr;
background: linear-gradient(180deg, color-mix(in oklab, var(--brand) 8%, transparent), transparent 60%);
border: 1px solid var(--outline);
border-radius: var(--radius);
padding: clamp(16px, 3vw, 28px);
}
.hero h1 { font-size: var(--fs-1); margin: 0 0 6px; }
.subtitle { font-size: var(--fs-3); color: var(--text-dim); }
.kpis {
display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gap);
container-type: inline-size;
}
@container (min-width: 600px) {
.kpis { grid-template-columns: repeat(4, 1fr); }
}
.kpi {
background: var(--surface); border: 1px solid var(--outline);
border-radius: 14px; padding: 16px;
display: grid; gap: 6px; align-content: start;
min-height: 110px;
}
.kpi b { font-size: clamp(22px, 4vw, 32px); }
.kpi small { color: var(--text-dim); }
.grid {
display: grid; gap: var(--gap);
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.layout { grid-template-columns: minmax(240px, var(--nav-w)) 1fr; align-items: start; }
.grid.twocol { grid-template-columns: 1fr 1fr; }
}
@media (min-width: 1024px) {
.grid.threecol { grid-template-columns: 1.4fr 1fr 1fr; }
}
aside.nav {
position: sticky; top: calc(62px + 6px);
align-self: start; height: calc(100dvh - 90px); overflow: auto;
padding-right: 6px;
}
.toc {
background: var(--surface); border: 1px solid var(--outline); border-radius: var(--radius);
padding: 12px;
}
.toc h4 { margin: 6px 8px 10px; font-size: 14px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .12em; }
.toc a {
display: grid; grid-template-columns: 18px 1fr auto; align-items: center;
gap: 10px; padding: 8px 10px; border-radius: 10px; color: var(--text-dim);
}
.toc a.active { background: color-mix(in oklab, var(--brand) 8%, transparent); color: var(--text); }
.toc a:hover { background: color-mix(in oklab, var(--brand) 12%, transparent); color: var(--text); }
main article {
display: grid; gap: var(--gap);
}
section.card {
background: var(--surface); border: 1px solid var(--outline);
border-radius: var(--radius); padding: clamp(14px, 2.5vw, 22px);
}
section.card h2, section.card h3 { margin: 0 0 8px; }
.muted { color: var(--text-dim); }
.badge {
font-size: 12px; padding: 3px 8px; border-radius: 12px; border: 1px solid var(--outline);
background: var(--surface-2); color: var(--text-dim);
}
.chart-card {
background: var(--surface); border: 1px dashed var(--outline);
border-radius: 14px; padding: 12px;
display: grid; gap: 8px;
}
.chart-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.chart-actions { display: flex; gap: 6px; flex-wrap: wrap; }
canvas.chart {
width: 100%; height: 280px; aspect-ratio: 16 / 9;
background: linear-gradient(180deg, color-mix(in oklab, var(--surface-2) 60%, transparent), transparent);
border-radius: 12px; border: 1px solid var(--outline);
}
.note { font-size: 12px; color: var(--text-dim); }
.flex { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.pill { padding: 4px 8px; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--outline); font-size: 12px; }
.insights {
display: grid; gap: 10px;
background: linear-gradient(135deg, color-mix(in oklab, var(--accent) 10%, transparent), transparent);
border: 1px solid var(--outline); border-radius: 12px; padding: 10px 12px;
}
.callout {
padding: 12px; border: 1px solid var(--outline); border-radius: 12px;
background: color-mix(in oklab, var(--brand) 6%, var(--surface));
}
.callout.warn { background: color-mix(in oklab, var(--warn) 12%, var(--surface)); }
.callout.danger { background: color-mix(in oklab, var(--danger) 12%, var(--surface)); }
.list {
display: grid; gap: 8px; padding-left: 18px;
}
.hr { height: 1px; background: var(--outline); margin: 8px 0; }
details[role="group"] {
border: 1px solid var(--outline); border-radius: 12px; padding: 8px 10px; background: var(--surface);
}
details[role="group"] > summary { cursor: pointer; list-style: none; }
details[role="group"] > summary::-webkit-details-marker { display: none; }
mark { background: color-mix(in oklab, var(--brand) 25%, transparent); color: currentColor; padding: 0 2px; border-radius: 4px; }
table {
width: 100%; border-collapse: collapse; border: 1px solid var(--outline); border-radius: 12px; overflow: clip;
background: var(--surface);
}
thead th {
position: sticky; top: 0; background: var(--surface-2);
color: var(--text-dim); font-weight: 600; text-align: left; font-size: 13px;
}
th, td { padding: 10px 12px; border-bottom: 1px solid var(--outline); }
th.sortable { cursor: pointer; }
.status {
display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 999px; font-size: 12px;
border: 1px solid var(--outline);
}
.status.ok { background: color-mix(in oklab, var(--ok) 10%, transparent); }
.status.warn { background: color-mix(in oklab, var(--warn) 10%, transparent); }
.status.danger { background: color-mix(in oklab, var(--danger) 10%, transparent); }
.source-tag { font-size: 12px; color: var(--text-dim); }
.footnote {
vertical-align: super; font-size: 11px; padding-left: 2px;
}
.rightbar {
display: none;
}
@media (min-width: 1024px) {
.layout {
grid-template-columns: minmax(240px, var(--nav-w)) 1fr minmax(280px, 360px);
}
.rightbar {
display: grid; gap: var(--gap);
position: sticky; top: calc(62px + 6px);
height: calc(100dvh - 90px); overflow: auto;
}
}
.footnotes a { word-break: break-word; }
.floating-top {
position: fixed; bottom: 18px; right: 18px; z-index: 60;
opacity: 0; transform: translateY(6px); transition: .2s;
}
.floating-top.show { opacity: 1; transform: translateY(0); }
.legend { display: flex; gap: 10px; flex-wrap: wrap; font-size: 12px; color: var(--text-dim); }
.legend .key { display: inline-flex; align-items: center; gap: 6px; }
.legend .swatch { width: 10px; height: 10px; border-radius: 3px; border: 1px solid var(--outline); }
.filters { display: flex; gap: 8px; flex-wrap: wrap; }
.filters .filter { display: inline-flex; gap: 6px; align-items: center; padding: 6px 10px; border-radius: 999px; border: 1px solid var(--outline); background: var(--surface-2); font-size: 12px; cursor: pointer; }
.filters .filter input { accent-color: var(--brand); }
.mini {
font-size: 12px; color: var(--text-dim);
}
.qr {
width: 100%; aspect-ratio: 1/1; border-radius: 10px;
background: repeating-linear-gradient(45deg, var(--muted) 0 12px, transparent 12px 24px), radial-gradient(circle at 30% 30%, color-mix(in oklab, var(--brand) 20%, transparent), transparent 40%), var(--surface);
border: 1px dashed var(--outline);
}
.export-bar { display: flex; gap: 8px; flex-wrap: wrap; }
.divider { height: 1px; background: var(--outline); margin: 8px 0 16px; }
.sr { position: absolute; left: -9999px; top: auto; width: 1px; height: 1px; overflow: hidden; }
</style>
</head>
<body>
<div class="app">
<header>
<div class="topbar">
<div class="brand">
<div class="logo" aria-hidden="true"><i class="ri-line-chart-line"></i></div>
Vietnam Economic Growth Report 2025
<span class="badge">Interactive</span>
</div>
<div class="search" role="search">
<i class="ri-search-2-line icon" aria-hidden="true"></i>
<input id="searchInput" type="search" placeholder="Search report, e.g. GDP, inflation, FDI…" autocomplete="off" aria-label="Search within report">
<span id="searchCount" class="mini"></span>
</div>
<div class="controls">
<button id="themeToggle" class="btn" aria-label="Toggle theme"><i class="ri-sun-line icon" aria-hidden="true"></i><span class="hide-mobile">Theme</span></button>
<div class="dropdown">
<button id="exportBtn" class="btn"><i class="ri-download-2-line icon" aria-hidden="true"></i>Export</button>
</div>
</div>
</div>
<div class="progress" id="progressBar" aria-hidden="true"></div>
</header>
<div class="layout">
<aside class="nav">
<nav class="toc" id="toc">
<h4>Contents</h4>
<div class="toc-list" role="list"></div>
</nav>
</aside>
<main>
<section class="hero">
<div>
<h1>Vietnam Economic Growth Report 2025</h1>
<p class="subtitle">Strong H1 momentum with 7.52% GDP growth; solid services and manufacturing performance amid global headwinds.<span class="footnote">[4,1]</span></p>
<div class="flex">
<span class="pill"><i class="ri-time-line"></i> Last updated: <span id="lastUpdated"></span></span>
<span class="pill"><i class="ri-book-2-line"></i> All figures include citations</span>
<span class="pill"><i class="ri-smartphone-line"></i> Mobile-optimized</span>
</div>
</div>
<div class="kpis">
<div class="kpi">
<small>Q2 2025 GDP (YoY)</small>
<b>7.96%</b>
<small class="source-tag">Source: GSO/Trading Economics<span class="footnote">[4,1]</span></small>
</div>
<div class="kpi">
<small>H1 2025 GDP (YoY)</small>
<b>7.52%</b>
<small class="source-tag">Source: GSO<span class="footnote">[4]</span></small>
</div>
<div class="kpi">
<small>Inflation (Jun 2025)</small>
<b>3.57%</b>
<small class="source-tag">Source: GSO/IMF/ADB<span class="footnote">[4,2,16]</span></small>
</div>
<div class="kpi">
<small>Unemployment (Q1 2025)</small>
<b>2.20%</b>
<small class="source-tag">Source: GSO<span class="footnote">[4]</span></small>
</div>
</div>
</section>
<article id="content">
<section class="card" id="exec-summary" data-title="Executive Summary">
<h2><i class="ri-article-line"></i> Executive Summary</h2>
<p>Vietnam’s economy sustained robust momentum in 2025. GDP rose 7.96% in Q2 (YoY) and 7.52% in H1—the strongest first-half since 2011—propelled by services and manufacturing despite global trade tensions and tariff pressures.<span class="footnote">[4,1]</span> Inflation remained contained within a 3–4.5% target band, unemployment stayed low at 2.20%, and FDI inflows accelerated, underscoring resilient fundamentals and investor confidence.<span class="footnote">[4,2,16,12,13]</span></p>
<div class="insights">
<div class="flex">
<span class="status ok"><i class="ri-arrow-up-circle-line"></i> Growth Momentum</span>
<span class="status ok"><i class="ri-shield-check-line"></i> Macro Stability</span>
<span class="status warn"><i class="ri-global-line"></i> External Risks</span>
</div>
<ul class="list">
<li>Baseline outlook: solid growth sustained; international forecasts (IMF, ADB) remain below government’s ambitious 8.3–8.5% target.<span class="footnote">[2,16,4]</span> See Key Indicators.</li>
<li>Policy stance emphasizes stability and resilience while enabling fiscal space for counter-cyclical support if global shocks intensify.<span class="footnote">[4,17]</span></li>
<li>Structural reliance on FDI merits attention alongside domestic capacity deepening to mitigate overdependence and inflation risks.<span class="footnote">[12,13]</span></li>
</ul>
</div>
</section>
<section class="card" id="methodology" data-title="Methodology">
<h2><i class="ri-flask-line"></i> Methodology</h2>
<p>This report synthesizes official statistics and reputable international sources. Data points are directly extracted from the provided research text and mapped to original sources listed in the References.</p>
<ul class="list">
<li>Primary data sources: Vietnam General Statistics Office (GSO), IMF, Asian Development Bank (ADB), Trading Economics, and Vietnam Investment Review.<span class="footnote">[4,2,16,1,12,13]</span></li>
<li>Processing: Key indicators structured into interactive charts and tables; section cross-references added; all statements include citations where applicable.</li>
<li>Visualization: All charts rendered client-side using Canvas/SVG; no external libraries used.</li>
</ul>
<div class="hr"></div>
<div class="mini">Note: No additional data has been inferred beyond what is provided; qualitative visuals (e.g., distribution counts) avoid implying undisclosed magnitudes.</div>
</section>
<section class="card" id="findings" data-title="Findings">
<h2><i class="ri-bar-chart-2-line"></i> Findings</h2>
<div class="grid twocol">
<div class="chart-card" id="gdp-line-card">
<div class="chart-head">
<div>
<h3 style="margin:0">GDP Growth — Q1 & Q2 2025</h3>
<div class="mini">YoY growth rates; H1 average shown as reference line.<span class="footnote">[4,1]</span></div>
</div>
<div class="chart-actions">
<button class="btn" data-download="gdpLine"><i class="ri-image-2-line"></i>PNG</button>
<button class="btn" data-copy="gdpLine"><i class="ri-clipboard-line"></i>Copy values</button>
</div>
</div>
<canvas id="gdpLine" class="chart" aria-label="Line chart: Vietnam GDP growth Q1 and Q2 2025" role="img"></canvas>
<div class="legend">
<span class="key"><span class="swatch" style="background: var(--brand)"></span> Q1–Q2 2025</span>
<span class="key"><span class="swatch" style="background: var(--accent)"></span> H1 2025 Avg (7.52%)</span>
</div>
<div class="note">Source: GSO; Trading Economics.<span class="footnote">[4,1]</span></div>
</div>
<div class="chart-card" id="forecast-bars-card">
<div class="chart-head">
<div>
<h3 style="margin:0">2025 GDP Growth Forecasts</h3>
<div class="mini">Institutional forecasts vs. government target band.<span class="footnote">[2,16,4,7]</span></div>
</div>
<div class="chart-actions">
<button class="btn" data-download="forecastBars"><i class="ri-image-2-line"></i>PNG</button>
<button class="btn" data-copy="forecastBars"><i class="ri-clipboard-line"></i>Copy values</button>
</div>
</div>
<canvas id="forecastBars" class="chart" aria-label="Bar chart: 2025 GDP growth forecasts by institution" role="img"></canvas>
<div class="legend">
<span class="key"><span class="swatch" style="background: var(--brand)"></span> Forecast</span>
<span class="key"><span class="swatch" style="background: var(--warn)"></span> Gov Target Min</span>
<span class="key"><span class="swatch" style="background: var(--danger)"></span> Gov Target Max</span>
</div>
<div class="note">Sources: IMF, ADB, FocusEconomics (aggregates); Government target (GSO/MPI).<span class="footnote">[2,16,7,4,17]</span></div>
</div>
<div class="chart-card" id="inflation-bars-card">
<div class="chart-head">
<div>
<h3 style="margin:0">Inflation — May & Jun 2025</h3>
<div class="mini">Monthly inflation with forecast reference lines.<span class="footnote">[4,2,16]</span></div>
</div>
<div class="chart-actions">
<button class="btn" data-download="inflationBars"><i class="ri-image-2-line"></i>PNG</button>
<button class="btn" data-copy="inflationBars"><i class="ri-clipboard-line"></i>Copy values</button>
</div>
</div>
<canvas id="inflationBars" class="chart" aria-label="Bar chart: May and June 2025 inflation with IMF and ADB forecasts" role="img"></canvas>
<div class="legend">
<span class="key"><span class="swatch" style="background: var(--brand)"></span> Actual</span>
<span class="key"><span class="swatch" style="background: var(--accent)"></span> IMF 2025 Forecast (2.9%)</span>
<span class="key"><span class="swatch" style="background: var(--brand-2)"></span> ADB 2025 Forecast (4.0%)</span>
</div>
<div class="note">Sources: GSO, IMF, ADB.<span class="footnote">[4,2,16]</span></div>
</div>
<div class="chart-card" id="fdi-bars-card">
<div class="chart-head">
<div>
<h3 style="margin:0">Foreign Direct Investment (FDI)</h3>
<div class="mini">Registered/disbursed (first 5 months) and total FDI (H1).<span class="footnote">[12,13]</span></div>
</div>
<div class="chart-actions">
<button class="btn" data-download="fdiBars"><i class="ri-image-2-line"></i>PNG</button>
<button class="btn" data-copy="fdiBars"><i class="ri-clipboard-line"></i>Copy values</button>
</div>
</div>
<canvas id="fdiBars" class="chart" aria-label="Bar chart: FDI registered, disbursed, and H1 total 2025" role="img"></canvas>
<div class="legend">
<span class="key"><span class="swatch" style="background: var(--brand)"></span> Registered (5M)</span>
<span class="key"><span class="swatch" style="background: var(--accent)"></span> Disbursed (5M)</span>
<span class="key"><span class="swatch" style="background: var(--warn)"></span> Total (H1)</span>
</div>
<div class="note">Sources: Vietnam Investment Review; Trading Economics (FDI series).<span class="footnote">[12,13]</span></div>
</div>
</div>
<div class="grid twocol">
<div class="chart-card" id="heatmap-card">
<div class="chart-head">
<div>
<h3 style="margin:0">Historical Q1 GDP Growth (2020–2025)</h3>
<div class="mini">YoY rates by year depicted as intensity heatmap.<span class="footnote">[1,4]</span></div>
</div>
<div class="chart-actions">
<button class="btn" data-download="q1Heatmap"><i class="ri-image-2-line"></i>PNG</button>
<button class="btn" data-copy="q1Heatmap"><i class="ri-clipboard-line"></i>Copy values</button>
</div>
</div>
<canvas id="q1Heatmap" class="chart" aria-label="Heatmap: Q1 GDP growth by year 2020–2025" role="img"></canvas>
<div class="legend">
<span class="key"><span class="swatch" style="background: #39427d"></span> Lower</span>
<span class="key"><span class="swatch" style="background: #5dd5ff"></span> Higher</span>
</div>
<div class="note">Sources: GSO; Trading Economics.<span class="footnote">[4,1]</span></div>
</div>
<div class="chart-card" id="pie-card">
<div class="chart-head">
<div>
<h3 style="margin:0">Forecast Distribution (By Band, Count of Institutions)</h3>
<div class="mini">Counts of forecasts ≤6%, 6–7%, ≥8% (Gov target band counted as ≥8%).<span class="footnote">[2,16,4,7,17]</span></div>
</div>
<div class="chart-actions">
<button class="btn" data-download="forecastPie"><i class="ri-image-2-line"></i>PNG</button>
<button class="btn" data-copy="forecastPie"><i class="ri-clipboard-line"></i>Copy values</button>
</div>
</div>
<canvas id="forecastPie" class="chart" aria-label="Pie chart: counts of institutions by forecast bands" role="img"></canvas>
<div class="legend">
<span class="key"><span class="swatch" style="background: var(--brand)"></span> ≤6%</span>
<span class="key"><span class="swatch" style="background: var(--accent)"></span> 6–7%</span>
<span class="key"><span class="swatch" style="background: var(--warn)"></span> ≥8%</span>
</div>
<div class="note">Based on counts (IMF, World Bank; ADB; Government target). Sources: IMF, ADB, FocusEconomics, GSO/MPI.<span class="footnote">[2,16,7,4,17]</span></div>
</div>
</div>
<div class="grid">
<div class="callout">
<b>Primary Growth Drivers</b>
<ul class="list">
<li>Services sector as major contributor to GDP growth.<span class="footnote">[4]</span></li>
<li>Manufacturing maintains recovery and development trajectory.<span class="footnote">[4]</span></li>
<li>Export industries continue as economic backbone despite trade challenges.<span class="footnote">[4,9,15]</span></li>
<li>Banking sector earnings projected +17% on ~15% system credit growth in 2025.<span class="footnote">[9,15]</span></li>
</ul>
</div>
<div class="callout">
<b>Retail Performance</b>
<p>Q1 2025 retail sales reached 1.708 quadrillion VND (~US$66.83bn), up 9.9% YoY, signaling resilient domestic demand.<span class="footnote">[4]</span></p>
</div>
</div>
</section>
<section class="card" id="risks" data-title="Challenges & Risks">
<h2><i class="ri-error-warning-line"></i> Challenges and Risk Factors</h2>
<div class="grid twocol">
<div class="callout warn">
<ul class="list">
<li>Global trade tensions and US tariffs weighing on export-oriented firms.<span class="footnote">[9,14,15]</span></li>
<li>Geopolitical instability raising uncertainty and investment risk premia.<span class="footnote">[14,2]</span></li>
<li>FDI overdependence and inflation vigilance flagged by experts.<span class="footnote">[12,13,15]</span></li>
<li>Growth must avoid compromising macro stability, debt dynamics, or inflation.<span class="footnote">[2,4,16]</span></li>
</ul>
</div>
<div class="chart-card">
<div class="chart-head">
<div>
<h3 style="margin:0">Labor Market — Unemployment Gauge</h3>
<div class="mini">Q1 2025 unemployment at 2.20% (historically low).<span class="footnote">[4]</span></div>
</div>
<div class="chart-actions">
<button class="btn" data-download="unempGauge"><i class="ri-image-2-line"></i>PNG</button>
</div>
</div>
<canvas id="unempGauge" class="chart" aria-label="Gauge chart: unemployment rate Q1 2025" role="img"></canvas>
<div class="legend">
<span class="key"><span class="swatch" style="background: var(--ok)"></span> Low</span>
<span class="key"><span class="swatch" style="background: var(--warn)"></span> Moderate</span>
<span class="key"><span class="swatch" style="background: var(--danger)"></span> Elevated</span>
</div>
</div>
</div>
</section>
<section class="card" id="outlook" data-title="Economic Outlook & Projections">
<h2><i class="ri-compass-3-line"></i> Economic Outlook and Projections</h2>
<div class="grid threecol">
<div class="callout">
<b>Near-term Prospects (2025)</b>
<p>Strong start in Q1 (6.9% YoY) with ongoing traction. Outlook remains constructive though headwinds persist; the government’s 8.3–8.5% target is ambitious relative to international forecasts.<span class="footnote">[4,2,16]</span></p>
</div>
<div class="callout">
<b>Supporting Factors</b>
<ul class="list">
<li>Robust FDI inflows and confidence.<span class="footnote">[12,13]</span></li>
<li>Low unemployment supporting consumption.<span class="footnote">[4]</span></li>
<li>Controlled inflation preserving purchasing power.<span class="footnote">[2,16,4]</span></li>
<li>Export competitiveness despite trade frictions.<span class="footnote">[9,15]</span></li>
<li>Parliamentary support for higher growth target.<span class="footnote">[17]</span></li>
</ul>
</div>
<div class="callout">
<b>Risk Mitigation</b>
<ul class="list">
<li>Diversify export markets; strengthen domestic demand.<span class="footnote">[4,17]</span></li>
<li>Enhance resilience and safeguard macro stability.<span class="footnote">[2,4,16]</span></li>
<li>Maintain fiscal space for counter-shock support.<span class="footnote">[2,4,16,17]</span></li>
</ul>
</div>
</div>
<div class="hr"></div>
<div class="mini">Historical context: 2024 GDP growth at 7.1%; long-term outlook resilient despite short-term headwinds.<span class="footnote">[1,4]</span></div>
</section>
<section class="card" id="tables" data-title="Data Tables & Filters">
<h2><i class="ri-table-line"></i> Data Tables & Filters</h2>
<div class="filters">
<label class="filter"><input type="checkbox" class="dataset-filter" data-filter="actual" checked> Actuals</label>
<label class="filter"><input type="checkbox" class="dataset-filter" data-filter="forecast" checked> Forecasts</label>
<label class="filter"><input type="checkbox" class="dataset-filter" data-filter="policy" checked> Targets/Policy</label>
</div>
<div class="hr"></div>
<div class="chart-card" style="padding:0">
<table id="indicatorsTable">
<thead>
<tr>
<th class="sortable" data-sort="text">Indicator</th>
<th class="sortable" data-sort="text">Period</th>
<th class="sortable" data-sort="num">Value</th>
<th class="sortable" data-sort="text">Type</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<!-- populated by JS -->
</tbody>
</table>
</div>
<div class="mini">Tip: Click table headers to sort. Use filters to show/hide categories.</div>
</section>
<section class="card" id="conclusion" data-title="Conclusion">
<h2><i class="ri-checkbox-circle-line"></i> Conclusion</h2>
<p>Vietnam’s 2025 performance highlights resilience and growth potential. With inflation contained, unemployment low, and FDI strong, the economy is positioned for continued expansion. The divergence between government targets and international forecasts argues for cautious optimism, balancing ambition with macroprudential discipline.<span class="footnote">[2,4,12,13,16]</span></p>
</section>
<section class="card" id="references" data-title="References">
<h2><i class="ri-graduation-cap-line"></i> Sources and Citations</h2>
<ol class="footnotes">
<li id="ref1">Trading Economics - Vietnam GDP Annual Growth Rate — https://tradingeconomics.com/vietnam/gdp-growth-annual <button class="btn ghost" data-copy-cite="1"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref2">International Monetary Fund - Vietnam Country Profile — https://www.imf.org/en/Countries/VNM <button class="btn ghost" data-copy-cite="2"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref3">World Economics - Vietnam GDP Estimates — https://www.worldeconomics.com/GDP/Vietnam.gdp <button class="btn ghost" data-copy-cite="3"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref4">Government of Vietnam - General Statistics Office — https://www.gso.gov.vn/en/ <button class="btn ghost" data-copy-cite="4"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref5">Wikipedia - Economy of Vietnam — https://en.wikipedia.org/wiki/Economy_of_Vietnam <button class="btn ghost" data-copy-cite="5"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref6">IMF - Vietnam and the IMF — https://www.imf.org/en/Countries/VNM <button class="btn ghost" data-copy-cite="6"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref7">FocusEconomics - Vietnam Economic Indicators — https://www.focus-economics.com/countries/vietnam <button class="btn ghost" data-copy-cite="7"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref8">National Statistics Office of Vietnam - Economic Reports — https://www.gso.gov.vn/en/data-and-statistics/ <button class="btn ghost" data-copy-cite="8"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref9">VietnamNet - Economic News and Analysis — https://vietnamnet.vn/ <button class="btn ghost" data-copy-cite="9"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref10">IMF - Article IV Mission Reports — https://www.imf.org/en/Publications/CR <button class="btn ghost" data-copy-cite="10"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref11">Vietnam Briefing - Economic Analysis — https://www.vietnam-briefing.com/ <button class="btn ghost" data-copy-cite="11"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref12">Vietnam Investment Review - FDI Statistics — https://vir.com.vn/ <button class="btn ghost" data-copy-cite="12"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref13">Trading Economics - Vietnam Foreign Direct Investment — https://tradingeconomics.com/vietnam/foreign-direct-investment <button class="btn ghost" data-copy-cite="13"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref14">White & Case - Regional Economic Outlook — https://www.whitecase.com/ <button class="btn ghost" data-copy-cite="14"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref15">Vietnam Economic Times — https://vneconomictimes.com/ <button class="btn ghost" data-copy-cite="15"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref16">Asian Development Bank - Vietnam Country Partnership — https://www.adb.org/countries/viet-nam/main <button class="btn ghost" data-copy-cite="16"><i class="ri-clipboard-line"></i> Copy</button></li>
<li id="ref17">Ministry of Planning and Investment - Vietnam — https://www.mpi.gov.vn/en/ <button class="btn ghost" data-copy-cite="17"><i class="ri-clipboard-line"></i> Copy</button></li>
</ol>
</section>
<section class="card" id="appendix" data-title="Appendices">
<h2><i class="ri-folders-line"></i> Appendices</h2>
<details role="group" open>
<summary class="flex"><b>Appendix A — Cross-References</b> <span class="badge">Navigator</span></summary>
<div class="grid">
<p>See: Executive Summary → Findings (Key Indicators) for the quantitative backbone; Risks section complements Outlook with policy actions. References anchor all figures to original sources.</p>
</div>
</details>
<details role="group" style="margin-top:10px;">
<summary class="flex"><b>Appendix B — Download Center</b> <span class="badge">Export</span></summary>
<div class="export-bar" style="margin-top:8px;">
<button class="btn primary" id="exportJSON"><i class="ri-braces-line"></i> Data (JSON)</button>
<button class="btn primary" id="exportCSV"><i class="ri-file-text-line"></i> Data (CSV)</button>
<button class="btn" id="printBtn"><i class="ri-printer-line"></i> Print / PDF</button>
</div>
<div class="divider"></div>
<div class="grid twocol">
<div>
<div class="mini">Share</div>
<button class="btn" id="copyLink"><i class="ri-link"></i> Copy page link</button>
</div>
<div>
<div class="mini">QR (placeholder)</div>
<div class="qr" aria-hidden="true"></div>
</div>
</div>
</details>
</section>
</article>
</main>
<aside class="rightbar">
<section class="card">
<h3><i class="ri-lightbulb-flash-line"></i> Key Insights</h3>
<ul class="list">
<li>H1 2025 GDP 7.52% — highest since 2011.<span class="footnote">[4]</span></li>
<li>Inflation contained (Jun: 3.57%); IMF 2.9%, ADB 4.0 for 2025.<span class="footnote">[2,16,4]</span></li>
<li>Unemployment near historical lows at 2.20%.<span class="footnote">[4]</span></li>
<li>FDI strong: US$21.51bn in H1 (+32.6% YoY).<span class="footnote">[12]</span></li>
</ul>
</section>
<section class="card">
<h3><i class="ri-question-answer-line"></i> Quick Facts</h3>
<ul class="list">
<li>Q1 growth: 6.9% YoY; Q2: 7.96% YoY.<span class="footnote">[4,1]</span></li>
<li>Retail Q1: 1.708 quadrillion VND (+9.9%).<span class="footnote">[4]</span></li>
<li>Gov growth target: 8.3–8.5% (raised to at least 8%).<span class="footnote">[17,4]</span></li>
</ul>
</section>
</aside>
</div>
<button class="btn floating-top" id="toTop"><i class="ri-arrow-up-line"></i> Top</button>
</div>
<div id="tooltip" class="btn" style="position: fixed; pointer-events: none; opacity: 0; transform: translate(-50%, -120%);"></div>
<script>
// Utilities
const $ = (sel, el=document) => el.querySelector(sel);
const $$ = (sel, el=document) => Array.from(el.querySelectorAll(sel));
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const fmt = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 });
const today = new Date();
$('#lastUpdated').textContent = today.toLocaleDateString();
// Theme toggle with LocalStorage
const themeToggle = $('#themeToggle');
const storedTheme = localStorage.getItem('theme');
if (storedTheme) document.body.classList.toggle('theme-light', storedTheme === 'light');
const setThemeIcon = () => {
const icon = themeToggle.querySelector('i');
if (document.body.classList.contains('theme-light')) {
icon.className = 'ri-moon-line icon';
} else {
icon.className = 'ri-sun-line icon';
}
};
setThemeIcon();
themeToggle.addEventListener('click', () => {
document.body.classList.toggle('theme-light');
localStorage.setItem('theme', document.body.classList.contains('theme-light') ? 'light' : 'dark');
setThemeIcon();
// redraw charts on theme change for crisp colors
drawAll();
});
// Reading progress
const progress = $('#progressBar');
document.addEventListener('scroll', () => {
const scrolled = window.scrollY;
const h = document.documentElement.scrollHeight - window.innerHeight;
const p = h > 0 ? scrolled / h : 0;
progress.style.transform = `scaleX(${p})`;
});
// Build TOC and scroll spy
const content = $('#content');
const sections = $$('section.card[id]', content);
const tocList = $('.toc-list');
sections.forEach(sec => {
const title = sec.dataset.title || sec.querySelector('h2')?.textContent.trim() || 'Section';
const id = sec.id;
const a = document.createElement('a');
a.href = `#${id}`;
a.innerHTML = `<i class="ri-hashtag"></i><span>${title}</span><i class="ri-arrow-right-s-line" aria-hidden="true"></i>`;
tocList.appendChild(a);
});
const tocLinks = $$('a', tocList);
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
tocLinks.forEach(l => l.classList.toggle('active', l.getAttribute('href') === `#${entry.target.id}`));
}
});
}, { rootMargin: '-40% 0px -55% 0px', threshold: 0.1 });
sections.forEach(sec => observer.observe(sec));
// Back to top visibility
const toTop = $('#toTop');
const hero = $('.hero');
const topObs = new IntersectionObserver(([e]) => {
toTop.classList.toggle('show', !e.isIntersecting);
}, { rootMargin: '-80px 0px 0px 0px' });
topObs.observe(hero);
toTop.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
// Tooltip
const tooltip = $('#tooltip');
function showTip(x, y, html) {
tooltip.innerHTML = html;
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
tooltip.style.opacity = 1;
}
function hideTip() { tooltip.style.opacity = 0; }
// Data (from report text)
const data = {
gdpQuarterly: [
{ label: 'Q1 2025', value: 6.9, cites: [4,1] },
{ label: 'Q2 2025', value: 7.96, cites: [4,1] },
],
gdpH1Avg: 7.52, // cites: [4]
forecasts: [
{ inst: 'IMF', value: 5.2, type: 'forecast', cites: [2] },
{ inst: 'ADB', value: 6.6, type: 'forecast', cites: [16] },
{ inst: 'World Bank', value: 5.8, type: 'forecast', cites: [7] }, // aggregated reference
{ inst: 'Gov Target Min', value: 8.3, type: 'policy', cites: [4,17] },
{ inst: 'Gov Target Max', value: 8.5, type: 'policy', cites: [4,17] },
],
inflation: {
months: [
{ label: 'May 2025', value: 3.24, cites: [4] },
{ label: 'Jun 2025', value: 3.57, cites: [4] },
],
forecasts: [
{ label: 'IMF 2025', value: 2.9, cites: [2] },
{ label: 'ADB 2025', value: 4.0, cites: [16] },
],
targetBand: [3.0, 4.5]
},
unemployment: { label: 'Q1 2025', value: 2.20, cites: [4] },
fdi: [
{ label: 'Registered (5M 2025)', value: 18.4, unit: 'US$ bn', cites: [12] },
{ label: 'Disbursed (5M 2025)', value: 8.9, unit: 'US$ bn', cites: [12] },
{ label: 'Total (H1 2025)', value: 21.51, unit: 'US$ bn', cites: [12] },
],
q1History: [
{ year: 2020, value: 3.21, cites: [1,4] },
{ year: 2021, value: 4.85, cites: [1,4] },
{ year: 2022, value: 5.42, cites: [1,4] },
{ year: 2023, value: 3.46, cites: [1,4] },
{ year: 2024, value: 5.98, cites: [1,4] },
{ year: 2025, value: 6.93, cites: [1,4] },
],
};
// Derived data for pie (counts by band)
const pieBands = [
{ label: '≤6%', count: 2, color: getCSS('--brand') }, // IMF 5.2, WB 5.8
{ label: '6–7%', count: 1, color: getCSS('--accent') }, // ADB 6.6
{ label: '≥8%', count: 1, color: getCSS('--warn') }, // Gov target band
];
// Canvas helpers
function getCSS(varName) {
return getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
}
function resizeCanvasToDisplaySize(canvas) {
const ratio = window.devicePixelRatio || 1;
const { width, height } = canvas.getBoundingClientRect();
const w = Math.round(width * ratio);
const h = Math.round(height * ratio);
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w; canvas.height = h;
return true;
}
return false;
}
function saveCanvasPNG(canvas, filename='chart.png') {
const a = document.createElement('a');
a.href = canvas.toDataURL('image/png');
a.download = filename;
a.click();
}
// Line chart: GDP
function drawGDPLine() {
const canvas = $('#gdpLine');
const ctx = canvas.getContext('2d');
resizeCanvasToDisplaySize(canvas);
const { width: W, height: H } = canvas;
ctx.clearRect(0, 0, W, H);
const pad = 60;
const dataPoints = data.gdpQuarterly.map(d => d.value);
const labels = data.gdpQuarterly.map(d => d.label);
const minV = Math.min(...dataPoints, data.gdpH1Avg) - 0.5;
const maxV = Math.max(...dataPoints, data.gdpH1Avg) + 0.5;
const xStep = (W - pad*2) / (dataPoints.length - 1 || 1);
const y = v => H - pad - ( (v - minV) / (maxV - minV) ) * (H - pad*2);
const x = i => pad + i * xStep;
// Grid
ctx.strokeStyle = getCSS('--outline');
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
for (let g = Math.ceil(minV); g <= Math.floor(maxV); g += 0.5) {
const gy = y(g);
ctx.beginPath(); ctx.moveTo(pad, gy); ctx.lineTo(W - pad, gy); ctx.stroke();
ctx.fillStyle = getCSS('--text-dim');
ctx.fillText(g.toFixed(1) + '%', 8, gy + 4);
}
ctx.setLineDash([]);
// H1 average line
ctx.strokeStyle = getCSS('--accent');
ctx.lineWidth = 2;
ctx.setLineDash([6,4]);
const avgY = y(data.gdpH1Avg);
ctx.beginPath(); ctx.moveTo(pad, avgY); ctx.lineTo(W - pad, avgY); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = getCSS('--text-dim');
ctx.fillText('H1 Avg ' + data.gdpH1Avg + '%', W - pad - 120, avgY - 6);
// Line
ctx.strokeStyle = getCSS('--brand');
ctx.lineWidth = 3;
ctx.beginPath();
dataPoints.forEach((v, i) => {
ctx.lineTo(x(i), y(v));
});
ctx.stroke();
// Points
ctx.fillStyle = getCSS('--brand');
dataPoints.forEach((v, i) => {
ctx.beginPath();
ctx.arc(x(i), y(v), 6, 0, Math.PI*2); ctx.fill();
});
// Axes labels
ctx.fillStyle = getCSS('--text');
ctx.textAlign = 'center';
labels.forEach((lab, i) => ctx.fillText(lab, x(i), H - pad + 18));
// Interaction
canvas.onmousemove = (e) => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (canvas.width / rect.width);
const my = (e.clientY - rect.top) * (canvas.height / rect.height);
let nearest = -1, dist = 1e9;
dataPoints.forEach((v, i) => {
const dx = x(i) - mx, dy = y(v) - my;
const d = Math.hypot(dx, dy);
if (d < dist) { dist = d; nearest = i; }
});
if (nearest !== -1 && dist < 60) {
showTip(e.clientX, e.clientY, `<b>${labels[nearest]}</b><br>${dataPoints[nearest]}%`);
} else hideTip();
};
canvas.onmouseleave = hideTip;
}
// Bar chart: Forecasts
function drawForecastBars() {
const canvas = $('#forecastBars');
const ctx = canvas.getContext('2d');
resizeCanvasToDisplaySize(canvas);
const { width: W, height: H } = canvas;
ctx.clearRect(0,0,W,H);
const pad = 60;
const items = data.forecasts;
const labels = items.map(d => d.inst);
const values = items.map(d => d.value);
const colors = items.map(d => d.type === 'policy' ? (d.inst.includes('Min') ? getCSS('--warn') : getCSS('--danger')) : getCSS('--brand'));
const minV = 0;
const maxV = Math.max(9, Math.max(...values) + 0.5);
const chartW = W - pad*2;
const barW = chartW / values.length * 0.6;
// y scale
const y = v => H - pad - (v - minV)/(maxV - minV)*(H - pad*2);
// grid
ctx.strokeStyle = getCSS('--outline');
ctx.setLineDash([4,4]);
for (let g = 0; g <= maxV; g += 1) {
const gy = y(g);
ctx.beginPath(); ctx.moveTo(pad, gy); ctx.lineTo(W - pad, gy); ctx.stroke();
ctx.fillStyle = getCSS('--text-dim');
ctx.fillText(g + '%', 22, gy + 4);
}
ctx.setLineDash([]);
// bars
ctx.textAlign = 'center';
labels.forEach((lab, i) => {
const cx = pad + (i + 0.5) * (chartW / values.length);
const v = values[i];
const top = y(v), bottom = y(0);
ctx.fillStyle = colors[i];
const bw = clamp(barW, 24, 80);
ctx.fillRect(cx - bw/2, top, bw, bottom - top);
ctx.fillStyle = getCSS('--text');
ctx.fillText(v + '%', cx, top - 6);
ctx.fillStyle = getCSS('--text-dim');
ctx.fillText(lab, cx, H - pad + 18);
});
canvas.onmousemove = (e) => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (canvas.width / rect.width);
const my = (e.clientY - rect.top) * (canvas.height / rect.height);
const chartX0 = pad, chartX1 = W - pad;
const idx = Math.floor((mx - chartX0) / ((chartX1 - chartX0) / values.length));
if (idx >= 0 && idx < values.length) {
showTip(e.clientX, e.clientY, `<b>${labels[idx]}</b><br>${values[idx]}%`);
} else hideTip();
};
canvas.onmouseleave = hideTip;
}
// Bar chart: Inflation with reference lines
function drawInflationBars() {
const canvas = $('#inflationBars');
const ctx = canvas.getContext('2d');
resizeCanvasToDisplaySize(canvas);
const { width: W, height: H } = canvas;
ctx.clearRect(0,0,W,H);
const pad = 60;
const items = data.inflation.months;
const labels = items.map(d => d.label);
const values = items.map(d => d.value);
const refIMF = data.inflation.forecasts[0].value;
const refADB = data.inflation.forecasts[1].value;
const minV = 0;
const maxV = Math.max(5, Math.max(...values, refIMF, refADB) + 0.5);
const chartW = W - pad*2;
const barW = chartW / values.length * 0.5;
const y = v => H - pad - (v - minV)/(maxV - minV)*(H - pad*2);
// grid
ctx.strokeStyle = getCSS('--outline'); ctx.setLineDash([4,4]);
for (let g = 0; g <= maxV; g += 0.5) {
const gy = y(g);
ctx.beginPath(); ctx.moveTo(pad, gy); ctx.lineTo(W - pad, gy); ctx.stroke();
ctx.fillStyle = getCSS('--text-dim'); ctx.fillText(g + '%', 22, gy + 4);
}
ctx.setLineDash([]);
// reference lines
ctx.lineWidth = 2;
ctx.strokeStyle = getCSS('--accent');
ctx.setLineDash([6,4]);
ctx.beginPath(); ctx.moveTo(pad, y(refIMF)); ctx.lineTo(W - pad, y(refIMF)); ctx.stroke();
ctx.fillStyle = getCSS('--text-dim'); ctx.fillText('IMF ' + refIMF + '%', W - pad - 100, y(refIMF) - 6);
ctx.strokeStyle = getCSS('--brand-2');
ctx.beginPath(); ctx.moveTo(pad, y(refADB)); ctx.lineTo(W - pad, y(refADB)); ctx.stroke();
ctx.fillStyle = getCSS('--text-dim'); ctx.fillText('ADB ' + refADB + '%', W - pad - 100, y(refADB) - 6);
ctx.setLineDash([]);
// bars
ctx.textAlign = 'center';
labels.forEach((lab, i) => {
const cx = pad + (i + 0.5) * (chartW / values.length);
const v = values[i];
const top = y(v), bottom = y(0);
ctx.fillStyle = getCSS('--brand');
const bw = clamp(barW, 28, 120);
ctx.fillRect(cx - bw/2, top, bw, bottom - top);
ctx.fillStyle = getCSS('--text');
ctx.fillText(v + '%', cx, top - 6);
ctx.fillStyle = getCSS('--text-dim');
ctx.fillText(lab, cx, H - pad + 18);
});
canvas.onmousemove = (e) => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (canvas.width / rect.width);
const chartX0 = pad, chartX1 = W - pad;
const idx = Math.floor((mx - chartX0) / ((chartX1 - chartX0) / values.length));
if (idx >= 0 && idx < values.length) {
showTip(e.clientX, e.clientY, `<b>${labels[idx]}</b><br>${values[idx]}%`);
} else hideTip();
};
canvas.onmouseleave = hideTip;
}
// Bar chart: FDI
function drawFDIBars() {
const canvas = $('#fdiBars');
const ctx = canvas.getContext('2d');
resizeCanvasToDisplaySize(canvas);
const { width: W, height: H } = canvas;
ctx.clearRect(0,0,W,H);
const pad = 60;
const items = data.fdi;
const labels = items.map(d => d.label);
const values = items.map(d => d.value);
const colors = [getCSS('--brand'), getCSS('--accent'), getCSS('--warn')];
const minV = 0; const maxV = Math.max(...values) + 5;
const y = v => H - pad - (v - minV)/(maxV - minV)*(H - pad*2);
const chartW = W - pad*2;
const barW = chartW / values.length * 0.5;
// grid
ctx.strokeStyle = getCSS('--outline'); ctx.setLineDash([4,4]);
for (let g = 0; g <= maxV; g += 5) {
const gy = y(g);
ctx.beginPath(); ctx.moveTo(pad, gy); ctx.lineTo(W - pad, gy); ctx.stroke();
ctx.fillStyle = getCSS('--text-dim'); ctx.fillText('US$ ' + g + 'bn', 8, gy + 4);
}
ctx.setLineDash([]);
// bars
ctx.textAlign = 'center';
labels.forEach((lab, i) => {
const cx = pad + (i + 0.5) * (chartW / values.length);
const v = values[i];
const top = y(v), bottom = y(0);
ctx.fillStyle = colors[i % colors.length];
const bw = clamp(barW, 28, 120);
ctx.fillRect(cx - bw/2, top, bw, bottom - top);
ctx.fillStyle = getCSS('--text');
ctx.fillText(v + ' bn', cx, top - 6);
ctx.fillStyle = getCSS('--text-dim');
ctx.fillText(lab, cx, H - pad + 18);
});
canvas.onmousemove = (e) => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (canvas.width / rect.width);
const idx = Math.floor((mx - (60)) / ((W - 120) / values.length));
if (idx >= 0 && idx < values.length) {
showTip(e.clientX, e.clientY, `<b>${labels[idx]}</b><br>US$ ${values[idx]} bn`);
} else hideTip();
};
canvas.onmouseleave = hideTip;
}
// Heatmap: Q1 history
function drawQ1Heatmap() {
const canvas = $('#q1Heatmap');
const ctx = canvas.getContext('2d');
resizeCanvasToDisplaySize(canvas);
const { width: W, height: H } = canvas;
ctx.clearRect(0,0,W,H);
const pad = 40;
const items = data.q1History;
const values = items.map(d => d.value);
const minV = Math.min(...values), maxV = Math.max(...values);
const tiles = items.length;
const gap = 8;
const tileW = (W - pad*2 - gap*(tiles-1)) / tiles;
const tileH = H - pad*2;
// color scale from deep to bright
function color(v) {
const t = (v - minV) / (maxV - minV + 1e-9);
const c1 = [57, 66, 125]; // low
const c2 = [93, 213, 255]; // high
const r = Math.round(c1[0] + t*(c2[0]-c1[0]));
const g = Math.round(c1[1] + t*(c2[1]-c1[1]));
const b = Math.round(c1[2] + t*(c2[2]-c1[2]));
return `rgb(${r}, ${g}, ${b})`;
}
// draw tiles
ctx.textAlign = 'center';
items.forEach((d, i) => {
const x = pad + i * (tileW + gap);
ctx.fillStyle = color(d.value);
const r = 12;
roundRect(ctx, x, pad, tileW, tileH, r, true, false);
ctx.fillStyle = getCSS('--text');
ctx.fillText(d.year, x + tileW/2, pad + tileH + 16);
ctx.fillText(d.value + '%', x + tileW/2, pad + tileH/2 + 4);
});
canvas.onmousemove = (e) => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (canvas.width / rect.width);
const my = (e.clientY - rect.top) * (canvas.height / rect.height);
for (let i=0;i<items.length;i++) {
const x = pad + i * (tileW + gap), y = pad;
if (mx >= x && mx <= x + tileW && my >= y && my <= y + tileH) {
showTip(e.clientX, e.clientY, `<b>${items[i].year}</b><br>${items[i].value}%`);
return;
}
}
hideTip();
};
canvas.onmouseleave = hideTip;
}
function roundRect(ctx, x, y, w, h, r, fill, stroke) {
if (typeof r === 'number') r = {tl:r,tr:r,br:r,bl:r};
ctx.beginPath();
ctx.moveTo(x + r.tl, y);
ctx.lineTo(x + w - r.tr, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r.tr);
ctx.lineTo(x + w, y + h - r.br);
ctx.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h);
ctx.lineTo(x + r.bl, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r.bl);
ctx.lineTo(x, y + r.tl);
ctx.quadraticCurveTo(x, y, x + r.tl, y);
ctx.closePath();
if (fill) ctx.fill();
if (stroke) ctx.stroke();
}
// Pie chart: counts
function drawForecastPie() {
const canvas = $('#forecastPie');
const ctx = canvas.getContext('2d');
resizeCanvasToDisplaySize(canvas);
const { width: W, height: H } = canvas;
ctx.clearRect(0,0,W,H);
const cx = W/2, cy = H/2, r = Math.min(W,H)/2 - 30;
const total = pieBands.reduce((a,b)=>a+b.count,0);
let start = -Math.PI/2;
pieBands.forEach(seg => {
const angle = (seg.count / total) * Math.PI*2;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.fillStyle = seg.color;
ctx.arc(cx, cy, r, start, start + angle);
ctx.closePath(); ctx.fill();
// label
const mid = start + angle/2;
const lx = cx + Math.cos(mid) * (r*0.6);
const ly = cy + Math.sin(mid) * (r*0.6);
ctx.fillStyle = getCSS('--text');
ctx.textAlign = 'center';
ctx.fillText(`${seg.label} (${seg.count})`, lx, ly);
start += angle;
});
canvas.onmousemove = (e) => {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width) - cx;
const y = (e.clientY - rect.top) * (canvas.height / rect.height) - cy;
const dist = Math.hypot(x,y);
if (dist > r) { hideTip(); return; }
let angle = Math.atan2(y,x);
if (angle < -Math.PI/2) angle += Math.PI*2;
let s = -Math.PI/2, found = null;
for (const seg of pieBands) {
const a = (seg.count/total)*Math.PI*2;
if (angle >= s && angle <= s + a) { found = seg; break; }
s += a;
}
if (found) showTip(e.clientX, e.clientY, `<b>${found.label}</b><br>${found.count} institution(s)`);
else hideTip();
};
canvas.onmouseleave = hideTip;
}
// Gauge: unemployment
function drawUnempGauge() {
const canvas = $('#unempGauge');
const ctx = canvas.getContext('2d');
resizeCanvasToDisplaySize(canvas);
const { width: W, height: H } = canvas;
ctx.clearRect(0,0,W,H);
const cx = W/2, cy = H*0.7, r = Math.min(W,H)*0.35;
const min = 0, max = 8; // display range
// bands
const bands = [
{ from: 0, to: 3, color: getCSS('--ok') },
{ from: 3, to: 5, color: getCSS('--warn') },
{ from: 5, to: 8, color: getCSS('--danger') },
];
function ang(v) { return Math.PI * (1 - (v - min)/(max - min)); }
bands.forEach(b => {
ctx.strokeStyle = b.color; ctx.lineWidth = 18;
ctx.beginPath();
ctx.arc(cx, cy, r, ang(b.to), ang(b.from), false);
ctx.stroke();
});
// tick marks
ctx.strokeStyle = getCSS('--outline'); ctx.lineWidth = 2;
for (let t=min; t<=max; t+=1) {
const a = ang(t), tx1 = cx + Math.cos(a)* (r-10), ty1 = cy + Math.sin(a)* (r-10);
const tx2 = cx + Math.cos(a)* (r-26), ty2 = cy + Math.sin(a)* (r-26);
ctx.beginPath(); ctx.moveTo(tx1, ty1); ctx.lineTo(tx2, ty2); ctx.stroke();
ctx.fillStyle = getCSS('--text-dim'); ctx.textAlign = 'center';
const tx = cx + Math.cos(a)*(r-40), ty = cy + Math.sin(a)*(r-40);
if (t % 2 === 0) ctx.fillText(t + '%', tx, ty+4);
}
// needle
const v = data.unemployment.value;
const a = ang(v);
ctx.strokeStyle = getCSS('--text'); ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(a)* (r-32), cy + Math.sin(a)*(r-32));
ctx.stroke();
ctx.fillStyle = getCSS('--text'); ctx.beginPath(); ctx.arc(cx, cy, 6, 0, Math.PI*2); ctx.fill();
// label
ctx.textAlign = 'center'; ctx.fillStyle = getCSS('--text');
ctx.font = '600 18px Inter, sans-serif'; ctx.fillText(v + '%', cx, cy - r + 36);
ctx.font = '400 14px Inter, sans-serif'; ctx.fillText(data.unemployment.label, cx, cy - r + 56);
canvas.onclick = () => saveCanvasPNG(canvas, 'unemployment_gauge.png');
}
// Populate big table with filters and sorting
const tableData = [
// Actuals
{ indicator: 'GDP Growth (Q1 2025)', period: 'Q1 2025', value: 6.9, unit: '%', type: 'actual', sources: [4,1] },
{ indicator: 'GDP Growth (Q2 2025)', period: 'Q2 2025', value: 7.96, unit: '%', type: 'actual', sources: [4,1] },
{ indicator: 'GDP Growth (H1 2025)', period: 'H1 2025', value: 7.52, unit: '%', type: 'actual', sources: [4] },
{ indicator: 'Inflation (May 2025)', period: 'May 2025', value: 3.24, unit: '%', type: 'actual', sources: [4] },
{ indicator: 'Inflation (Jun 2025)', period: 'Jun 2025', value: 3.57, unit: '%', type: 'actual', sources: [4] },
{ indicator: 'Unemployment', period: 'Q1 2025', value: 2.20, unit: '%', type: 'actual', sources: [4] },
{ indicator: 'FDI Registered', period: 'First 5M 2025', value: 18.4, unit: 'US$ bn', type: 'actual', sources: [12] },
{ indicator: 'FDI Disbursed', period: 'First 5M 2025', value: 8.9, unit: 'US$ bn', type: 'actual', sources: [12] },
{ indicator: 'FDI Total', period: 'H1 2025', value: 21.51, unit: 'US$ bn', type: 'actual', sources: [12] },
// Forecasts
{ indicator: 'GDP Growth 2025 (IMF)', period: '2025', value: 5.2, unit: '%', type: 'forecast', sources: [2] },
{ indicator: 'GDP Growth 2025 (ADB)', period: '2025', value: 6.6, unit: '%', type: 'forecast', sources: [16] },
{ indicator: 'GDP Growth 2025 (World Bank)', period: '2025', value: 5.8, unit: '%', type: 'forecast', sources: [7] },
{ indicator: 'Inflation 2025 (IMF)', period: '2025', value: 2.9, unit: '%', type: 'forecast', sources: [2] },
{ indicator: 'Inflation 2025 (ADB)', period: '2025', value: 4.0, unit: '%', type: 'forecast', sources: [16] },
// Policy/Targets
{ indicator: 'GDP Growth Target Min (Gov)', period: '2025', value: 8.3, unit: '%', type: 'policy', sources: [4,17] },
{ indicator: 'GDP Growth Target Max (Gov)', period: '2025', value: 8.5, unit: '%', type: 'policy', sources: [4,17] },
];
const tbody = $('#indicatorsTable tbody');
function renderTable() {
const active = new Set($$('.dataset-filter:checked').map(x => x.dataset.filter));
tbody.innerHTML = '';
tableData
.filter(r => active.has(r.type))
.forEach(r => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${r.indicator}</td>
<td>${r.period}</td>
<td data-value="${r.value}"><b>${fmt.format(r.value)}</b> <span class="mini">${r.unit || ''}</span></td>
<td><span class="status ${r.type==='actual'?'ok':(r.type==='forecast'?'warn':'danger')}">${r.type}</span></td>
<td class="mini">[${r.sources.join(', ')}]</td>
`;
tbody.appendChild(tr);
});
}
renderTable();
$$('.dataset-filter').forEach(cb => cb.addEventListener('change', renderTable));
// Sortable headers
let sortState = { idx: 0, dir: 1 };
$$('#indicatorsTable thead th.sortable').forEach((th, i) => {
th.addEventListener('click', () => {
const type = th.dataset.sort;
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a,b) => {
let va, vb;
if (i === 2) { // value col
va = parseFloat(a.children[i].dataset.value);
vb = parseFloat(b.children[i].dataset.value);
} else {
va = a.children[i].textContent.trim().toLowerCase();
vb = b.children[i].textContent.trim().toLowerCase();
}
return (va > vb ? 1 : va < vb ? -1 : 0) * (sortState.dir);
});
sortState.dir *= -1;
rows.forEach(r => tbody.appendChild(r));
});
});
// Search within document with highlight
const searchInput = $('#searchInput');
const searchCount = $('#searchCount');
let lastSearch = '';
function clearMarks() {
$$('.mark-wrap').forEach(span => {
const parent = span.parentNode;
parent.replaceChild(document.createTextNode(span.textContent), span);
parent.normalize();
});
}
function highlight(term) {
clearMarks();
if (!term) { searchCount.textContent = ''; return; }
let count = 0;
const walk = document.createTreeWalker($('#content'), NodeFilter.SHOW_TEXT, {
acceptNode(node) {
const t = node.nodeValue.trim();
if (!t) return NodeFilter.FILTER_REJECT;
if (node.parentElement.closest('code, pre, script, style')) return NodeFilter.FILTER_REJECT;
return t.toLowerCase().includes(term.toLowerCase()) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
});
const matches = [];
while (walk.nextNode()) matches.push(walk.currentNode);
matches.forEach(node => {
const idx = node.nodeValue.toLowerCase().indexOf(term.toLowerCase());
if (idx >= 0) {
const span = document.createElement('span'); span.className = 'mark-wrap';
const before = node.nodeValue.slice(0, idx);
const hit = node.nodeValue.slice(idx, idx + term.length);
const after = node.nodeValue.slice(idx + term.length);
span.innerHTML = `${before}<mark>${hit}</mark>${after}`;
node.parentNode.replaceChild(span, node);
count++;
}
});
searchCount.textContent = count ? `${count} result(s)` : '0 results';
}
let searchTimer;
searchInput.addEventListener('input', () => {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => highlight(searchInput.value.trim()), 120);
});
// Copy citation buttons
$$('[data-copy-cite]').forEach(btn => {
btn.addEventListener('click', () => {
const n = btn.dataset.copyCite;
const li = document.getElementById('ref'+n);
const text = li.textContent.replace(' Copy','').trim();
navigator.clipboard.writeText(text);
btn.innerHTML = '<i class="ri-check-line"></i> Copied';
setTimeout(()=>btn.innerHTML='<i class="ri-clipboard-line"></i> Copy', 1200);
});
});
// Export controls
$('#exportBtn').addEventListener('click', () => {
window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'});
});
$('#exportJSON').addEventListener('click', () => {
const blob = new Blob([JSON.stringify({ data, tableData }, null, 2)], { type: 'application/json' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'vietnam_econ_2025.json'; a.click();
});
$('#exportCSV').addEventListener('click', () => {
const header = ['indicator','period','value','unit','type','sources'];
const rows = tableData.map(r => [r.indicator, r.period, r.value, r.unit||'', r.type, r.sources.join('|')]);
const csv = [header.join(','), ...rows.map(r => r.map(v => `"${String(v).replaceAll('"','""')}"`).join(','))].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'vietnam_econ_2025.csv'; a.click();
});
$('#printBtn').addEventListener('click', () => window.print());
$('#copyLink').addEventListener('click', () => {
navigator.clipboard.writeText(location.href);
alert('Link copied to clipboard');
});
// Chart download/copy handlers
$$('[data-download]').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-download');
saveCanvasPNG(document.getElementById(id), `${id}.png`);
});
});
$$('[data-copy]').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-copy');
let values = '';
if (id === 'gdpLine') values = data.gdpQuarterly.map(d=>`${d.label}: ${d.value}%`).join('\n') + `\nH1 Avg: ${data.gdpH1Avg}%`;
if (id === 'forecastBars') values = data.forecasts.map(d=>`${d.inst}: ${d.value}% (${d.type})`).join('\n');
if (id === 'inflationBars') {
values = data.inflation.months.map(d=>`${d.label}: ${d.value}%`).join('\n')
+ `\nIMF 2025: ${data.inflation.forecasts[0].value}%\nADB 2025: ${data.inflation.forecasts[1].value}%`;
}
if (id === 'fdiBars') values = data.fdi.map(d=>`${d.label}: US$ ${d.value} bn`).join('\n');
if (id === 'q1Heatmap') values = data.q1History.map(d=>`${d.year}: ${d.value}%`).join('\n');
if (id === 'forecastPie') values = pieBands.map(d=>`${d.label}: ${d.count}`).join('\n');
navigator.clipboard.writeText(values);
btn.innerHTML = '<i class="ri-check-line"></i>Copied';
setTimeout(()=>btn.innerHTML = '<i class="ri-clipboard-line"></i>Copy values', 1200);
});
});
// Draw all charts (redraw on resize/theme)
function drawAll() {
drawGDPLine();
drawForecastBars();
drawInflationBars();
drawFDIBars();
drawQ1Heatmap();
drawForecastPie();
drawUnempGauge();
}
drawAll();
window.addEventListener('resize', () => { drawAll(); });
// Accessibility: keyboard skip to search
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
e.preventDefault(); searchInput.focus();
}
});
// Smooth TOC highlight on hashchange
window.addEventListener('hashchange', () => {
const id = location.hash.replace('#','');
tocLinks.forEach(a => a.classList.toggle('active', a.getAttribute('href') === '#'+id));
});
</script>
</body>
</html>