Spaces:
Running
Running
<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 Dashboard</title> | |
<!-- Google Fonts --> | |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&display=swap" rel="stylesheet"> | |
<!-- Feather Icons --> | |
<script src="https://unpkg.com/feather-icons"></script> | |
<style> | |
/* CSS Variables */ | |
:root{ | |
--bg: #f6f8fb; | |
--card: #ffffff; | |
--muted: #6b7280; | |
--accent: #0f766e; | |
--accent-2: #06b6d4; | |
--danger: #ef4444; | |
--glass: rgba(255,255,255,0.6); | |
--radius: 14px; | |
--shadow: 0 8px 30px rgba(16,24,40,0.06); | |
--max-width: 1200px; | |
--ff: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; | |
} | |
/* Reset & base */ | |
*{box-sizing:border-box} | |
html,body{height:100%} | |
body{ | |
margin:0; | |
font-family:var(--ff); | |
background:linear-gradient(180deg,#f7fbfc 0%, var(--bg) 100%); | |
color:#0f172a; | |
-webkit-font-smoothing:antialiased; | |
-moz-osx-font-smoothing:grayscale; | |
line-height:1.45; | |
padding:20px; | |
} | |
a{color:var(--accent); text-decoration:none} | |
small{color:var(--muted)} | |
/* Layout */ | |
.app{ | |
max-width:var(--max-width); | |
margin:0 auto; | |
display:grid; | |
gap:18px; | |
grid-template-columns: 1fr; | |
} | |
/* Header */ | |
header{ | |
display:flex; | |
gap:16px; | |
align-items:center; | |
justify-content:space-between; | |
background:linear-gradient(90deg, rgba(6,182,212,0.08), rgba(15,118,110,0.06)); | |
border-radius:var(--radius); | |
padding:16px; | |
box-shadow:var(--shadow); | |
position:sticky; | |
top:20px; | |
z-index:50; | |
backdrop-filter: blur(6px); | |
} | |
.brand{display:flex; gap:12px; align-items:center} | |
.logo{ | |
width:56px; | |
height:56px; | |
background:linear-gradient(135deg,var(--accent),var(--accent-2)); | |
color:white; | |
display:grid; | |
place-items:center; | |
font-weight:800; | |
border-radius:12px; | |
box-shadow:0 6px 18px rgba(6,182,212,0.14); | |
} | |
.title h1{margin:0;font-size:clamp(1.05rem,1.6vw,1.3rem)} | |
.title p{margin:0;color:var(--muted);font-size:0.86rem} | |
/* Toolbar */ | |
.tools{display:flex;gap:10px;align-items:center} | |
.btn{ | |
display:inline-flex; | |
gap:8px; | |
align-items:center; | |
border:0; | |
padding:8px 12px; | |
background:var(--card); | |
border-radius:10px; | |
box-shadow:var(--shadow); | |
cursor:pointer; | |
font-weight:600; | |
} | |
.btn.ghost{background:transparent;box-shadow:none} | |
.searchbar{ | |
display:flex; | |
align-items:center; | |
gap:8px; | |
border-radius:12px; | |
padding:8px; | |
background:var(--card); | |
box-shadow:var(--shadow); | |
min-width:180px; | |
} | |
.searchbar input{ | |
border:0;background:transparent;outline:none;font-size:0.95rem; | |
width:160px; | |
} | |
/* Core layout: TOC + content */ | |
.content{ | |
display:grid; | |
grid-template-columns: 280px 1fr; | |
gap:18px; | |
align-items:start; | |
} | |
/* Mobile adjustments */ | |
@media (max-width: 1024px){ | |
.content{grid-template-columns: 220px 1fr} | |
} | |
@media (max-width: 768px){ | |
.content{grid-template-columns: 1fr} | |
header{position:static} | |
} | |
/* TOC */ | |
nav.toc{ | |
position:sticky; | |
top:110px; | |
height:calc(100vh - 150px); | |
overflow:auto; | |
background:linear-gradient(180deg, rgba(255,255,255,0.9), rgba(255,255,255,0.7)); | |
border-radius:12px; | |
padding:12px; | |
box-shadow:var(--shadow); | |
scrollbar-width:thin; | |
} | |
nav.toc h3{margin:0 0 8px 0;font-size:0.95rem} | |
nav.toc ul{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px} | |
nav.toc a{ | |
padding:8px 10px; | |
border-radius:8px; | |
color:var(--muted); | |
display:flex;gap:8px;align-items:center; | |
font-weight:600; | |
} | |
nav.toc a.active{background:linear-gradient(90deg, rgba(6,182,212,0.08), rgba(15,118,110,0.04)); color:var(--accent)} | |
.toc-footer{margin-top:12px;font-size:0.85rem;color:var(--muted)} | |
/* Main article */ | |
main.report{ | |
display:flex; | |
flex-direction:column; | |
gap:18px; | |
} | |
/* Section card */ | |
section.card{ | |
background:var(--card); | |
border-radius:var(--radius); | |
padding:16px; | |
box-shadow:var(--shadow); | |
display:grid; | |
gap:14px; | |
} | |
.card-header{display:flex;align-items:center;justify-content:space-between;gap:12px} | |
.card-header h2{margin:0;font-size:1.05rem} | |
.meta{color:var(--muted);font-size:0.9rem} | |
/* KPI grid */ | |
.kpis{display:grid;grid-template-columns:repeat(2,1fr);gap:12px} | |
@media (min-width:1024px){ .kpis{grid-template-columns:repeat(4,1fr)} } | |
.kpi{ | |
background:linear-gradient(180deg, rgba(6,182,212,0.03), rgba(15,118,110,0.02)); | |
padding:12px;border-radius:12px;display:flex;flex-direction:column;gap:6px; | |
} | |
.kpi small{color:var(--muted)} | |
.kpi .value{font-weight:800;font-size:1.2rem;color:var(--accent)} | |
/* Charts area */ | |
.charts{display:grid;grid-template-columns:1fr;gap:12px} | |
@media (min-width:768px){ .charts{grid-template-columns:1fr 340px} } | |
.chart{ | |
background:linear-gradient(180deg, rgba(255,255,255,0.8), rgba(255,255,255,0.95)); | |
padding:12px;border-radius:12px; | |
display:flex;flex-direction:column;gap:8px; | |
} | |
.chart canvas, .chart svg{width:100%;height:220px} | |
.legend{display:flex;gap:12px;flex-wrap:wrap;font-size:0.88rem;color:var(--muted)} | |
/* Table */ | |
table{width:100%;border-collapse:collapse;font-size:0.95rem} | |
th,td{padding:10px;text-align:left;border-bottom:1px solid #eef2f7} | |
th{cursor:pointer;background:transparent;color:var(--muted);font-weight:700} | |
tr:hover td{background:linear-gradient(90deg,rgba(6,182,212,0.03),transparent)} | |
/* Controls row */ | |
.controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap} | |
.chip{background:var(--glass);padding:8px;border-radius:999px;font-weight:700;color:var(--accent);display:inline-flex;gap:8px;align-items:center} | |
/* Progress & indicators */ | |
.progress{ | |
height:10px;background:linear-gradient(90deg,#e6eef0,#f8fafb); | |
border-radius:999px;overflow:hidden; | |
} | |
.progress > i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--accent),var(--accent-2));transition:width 300ms} | |
/* Collapsible details styled */ | |
details summary{list-style:none;cursor:pointer;outline:none} | |
details summary::-webkit-details-marker{display:none} | |
details summary{display:flex;align-items:center;gap:8px;padding:10px;background:linear-gradient(180deg,#fff,#fbfeff);border-radius:10px} | |
details[open] summary{background:linear-gradient(90deg, rgba(6,182,212,0.06), rgba(15,118,110,0.03))} | |
/* Footer */ | |
footer{display:flex;justify-content:space-between;align-items:center;padding:12px;color:var(--muted);font-size:0.9rem} | |
/* Small utilities */ | |
.muted{color:var(--muted)} | |
.pill{padding:6px 10px;border-radius:999px;background:rgba(15,118,110,0.08);color:var(--accent);font-weight:700} | |
/* container queries for cards */ | |
.card { container-type: inline-size; } | |
@container (min-width: 420px) { | |
.kpi .label{font-size:0.95rem} | |
} | |
/* Tiny responsiveness */ | |
@media (max-width:420px){ | |
.kpis{grid-template-columns:repeat(1,1fr)} | |
.searchbar input{width:90px} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="app" id="app"> | |
<header> | |
<div class="brand"> | |
<div class="logo">VN</div> | |
<div class="title"> | |
<h1>Vietnam Economic Growth Report 2025</h1> | |
<p>Interactive analysis • Data-driven insights • Sources integrated</p> | |
</div> | |
</div> | |
<div class="tools"> | |
<div class="searchbar" title="Search report"> | |
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M21 21l-4.35-4.35" stroke="#6b7280" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><circle cx="11" cy="11" r="6" stroke="#6b7280" stroke-width="1.6"/></svg> | |
<input id="global-search" placeholder="Search sections, numbers..." /> | |
</div> | |
<button class="btn" id="copySummary" title="Copy executive summary to clipboard"> | |
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><rect x="9" y="9" width="11" height="11" rx="2" stroke="#0f766e" stroke-width="1.6"/><rect x="4" y="4" width="11" height="11" rx="2" stroke="#0f766e" stroke-width="1.6"/></svg> | |
Export | |
</button> | |
<button class="btn ghost" id="toggleFilters" title="Toggle data filters"> | |
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M22 6H2l7 8v6l6-4v-2l7-8z" stroke="#0f766e" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg> | |
Filters | |
</button> | |
</div> | |
</header> | |
<div class="content"> | |
<nav class="toc" aria-label="Table of contents"> | |
<h3>Contents</h3> | |
<ul id="toc-list"> | |
<li><a href="#executive" data-target="executive" class="toc-link active"><svg width="14" height="14"><circle cx="7" cy="7" r="6" fill="#06b6d4"/></svg> Executive Summary</a></li> | |
<li><a href="#indicators" data-target="indicators" class="toc-link"><svg width="14" height="14"><circle cx="7" cy="7" r="6" fill="#0f766e"/></svg> Key Indicators</a></li> | |
<li><a href="#sector" data-target="sector" class="toc-link"><svg width="14" height="14"><circle cx="7" cy="7" r="6" fill="#f59e0b"/></svg> Sectoral Analysis</a></li> | |
<li><a href="#risks" data-target="risks" class="toc-link"><svg width="14" height="14"><circle cx="7" cy="7" r="6" fill="#ef4444"/></svg> Challenges & Risks</a></li> | |
<li><a href="#history" data-target="history" class="toc-link"><svg width="14" height="14"><circle cx="7" cy="7" r="6" fill="#7c3aed"/></svg> Historical Comparison</a></li> | |
<li><a href="#outlook" data-target="outlook" class="toc-link"><svg width="14" height="14"><circle cx="7" cy="7" r="6" fill="#10b981"/></svg> Outlook & Projections</a></li> | |
<li><a href="#references" data-target="references" class="toc-link"><svg width="14" height="14"><circle cx="7" cy="7" r="6" fill="#94a3b8"/></svg> Sources & Citations</a></li> | |
</ul> | |
<div class="toc-footer"> | |
<div style="margin-bottom:8px">Reading progress</div> | |
<div class="progress" aria-hidden><i id="progress-bar"></i></div> | |
<div style="margin-top:10px;font-size:0.86rem">Saved filters: <span id="savedFilters" class="pill">None</span></div> | |
</div> | |
</nav> | |
<main class="report" id="report"> | |
<!-- Executive Summary --> | |
<section id="executive" class="card" data-title="Executive Summary"> | |
<div class="card-header"> | |
<div> | |
<h2>Executive Summary</h2> | |
<div class="meta">Snapshot of Vietnam's economic performance — H1 2025 and Q2 highlights</div> | |
</div> | |
<div class="controls"> | |
<span class="chip">Top-line: 7.52% H1 GDP</span> | |
<button class="btn" id="copyExec" title="Copy Executive Summary"><svg width="14" height="14" viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2" stroke="#0f766e" stroke-width="1.6"/><rect x="4" y="4" width="11" height="11" rx="2" stroke="#0f766e" stroke-width="1.6"/></svg> Copy</button> | |
</div> | |
</div> | |
<div style="display:grid;gap:12px"> | |
<p id="exec-text" class="muted"> | |
Vietnam's economy demonstrates robust growth in 2025 with GDP expanding 7.96% in Q2 and 7.52% in the first half — the strongest H1 performance since 2011. | |
Growth is led by services and manufacturing, supported by strong FDI inflows, low unemployment and controlled inflation. External risks from trade tensions and tariff policies remain. | |
</p> | |
<div style="display:grid;gap:10px;grid-template-columns:1fr 1fr"> | |
<div class="kpi"> | |
<small>Q2 2025 GDP (y/y)</small> | |
<div class="value" id="k-gdp-q2">7.96%</div> | |
<small class="muted">Source: GSO / aggregated</small> | |
</div> | |
<div class="kpi"> | |
<small>H1 2025 GDP (y/y)</small> | |
<div class="value" id="k-gdp-h1">7.52%</div> | |
<small class="muted">Highest mid-year since 2011</small> | |
</div> | |
</div> | |
<details> | |
<summary> | |
<strong>Key takeaways</strong> | |
<span style="margin-left:auto;color:var(--muted)">Click to expand</span> | |
</summary> | |
<div style="padding:10px;display:grid;gap:8px"> | |
<ul> | |
<li>Strong FDI: US$21.51 billion in H1 (up 32.6% y/y) — supporting manufacturing and exports.</li> | |
<li>Inflation remains contained (May 3.24%; June 3.57%) within target range.</li> | |
<li>Unemployment low at 2.20% in Q1, underpinning domestic consumption.</li> | |
</ul> | |
</div> | |
</details> | |
</div> | |
</section> | |
<!-- Key Indicators --> | |
<section id="indicators" class="card" data-title="Key Indicators"> | |
<div class="card-header"> | |
<div> | |
<h2>Key Indicators 2025</h2> | |
<div class="meta">Actuals, forecasts and comparative metrics</div> | |
</div> | |
<div class="controls"> | |
<button class="btn ghost" id="toggleForecasts"><svg width="14" height="14" viewBox="0 0 24 24"><path d="M3 12h18" stroke="#0f766e" stroke-width="1.6" stroke-linecap="round"/><path d="M3 6h12" stroke="#0f766e" stroke-width="1.6" stroke-linecap="round"/><path d="M3 18h8" stroke="#0f766e" stroke-width="1.6" stroke-linecap="round"/></svg> Forecasts</button> | |
<button class="btn" id="exportCSV"><svg width="14" height="14" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" stroke="#0f766e" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><polyline points="7 10 12 15 17 10" stroke="#0f766e" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><line x1="12" y1="15" x2="12" y2="3" stroke="#0f766e" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg> Export CSV</button> | |
</div> | |
</div> | |
<div class="kpis" id="indicator-kpis"> | |
<div class="kpi"> | |
<small>GDP Q1 2025 (y/y)</small> | |
<div class="value">6.90%</div> | |
<small class="muted">Actual</small> | |
</div> | |
<div class="kpi"> | |
<small>GDP Q2 2025 (y/y)</small> | |
<div class="value">7.96%</div> | |
<small class="muted">Actual</small> | |
</div> | |
<div class="kpi"> | |
<small>Inflation (Jun 2025)</small> | |
<div class="value">3.57%</div> | |
<small class="muted">IMF: 2.9% • ADB: 4.0%</small> | |
</div> | |
<div class="kpi"> | |
<small>Unemployment (Q1 2025)</small> | |
<div class="value">2.20%</div> | |
<small class="muted">Low level</small> | |
</div> | |
<div class="kpi"> | |
<small>FDI Registered (first 5 months)</small> | |
<div class="value">$18.4B</div> | |
<small class="muted">Up 51% y/y</small> | |
</div> | |
<div class="kpi"> | |
<small>FDI Disbursed (first 5 months)</small> | |
<div class="value">$8.9B</div> | |
<small class="muted"></small> | |
</div> | |
<div class="kpi"> | |
<small>Banking sector earnings (2025 est.)</small> | |
<div class="value">+17%</div> | |
<small class="muted">Credit growth supporting margins</small> | |
</div> | |
<div class="kpi"> | |
<small>Government GDP target 2025</small> | |
<div class="value">8.3–8.5%</div> | |
<small class="muted">Ambitious vs intl forecasts</small> | |
</div> | |
</div> | |
<div class="charts"> | |
<div class="chart" aria-hidden> | |
<div style="display:flex;justify-content:space-between;align-items:center"> | |
<strong>GDP Growth — Q1 (2020–2025)</strong> | |
<small class="muted">Interactive: hover for values</small> | |
</div> | |
<svg id="gdp-sparkline" viewBox="0 0 600 220" preserveAspectRatio="none" style="aspect-ratio: 3 / 1;"></svg> | |
<div class="legend" id="gdp-legend"></div> | |
</div> | |
<div class="chart"> | |
<div style="display:flex;justify-content:space-between;align-items:center"> | |
<strong>Current Periods</strong> | |
<small class="muted">Click bars to filter table</small> | |
</div> | |
<svg id="period-bars" viewBox="0 0 400 220" preserveAspectRatio="none" style="aspect-ratio: 1.2 / 1;"></svg> | |
<div style="display:flex;gap:8px;align-items:center;justify-content:space-between"> | |
<div class="muted">Q1, Q2, H1 comparison</div> | |
<div style="display:flex;gap:8px"> | |
<button id="showAll" class="btn ghost">Reset</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div style="margin-top:8px"> | |
<h3 style="margin:0 0 8px 0">Key indicators table</h3> | |
<div style="overflow:auto;border-radius:8px"> | |
<table id="indicators-table" aria-label="Key indicators"> | |
<thead> | |
<tr> | |
<th data-key="indicator">Indicator</th> | |
<th data-key="value">Value</th> | |
<th data-key="period">Period</th> | |
<th data-key="source">Source</th> | |
</tr> | |
</thead> | |
<tbody id="indicators-body"> | |
<!-- populated by JS --> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
</section> | |
<!-- Sectoral Analysis --> | |
<section id="sector" class="card" data-title="Sectoral Analysis"> | |
<div class="card-header"> | |
<div> | |
<h2>Sectoral Analysis</h2> | |
<div class="meta">Primary growth drivers and retail performance</div> | |
</div> | |
<div class="controls"> | |
<span class="pill">Services lead</span> | |
</div> | |
</div> | |
<div style="display:grid;gap:12px"> | |
<div style="display:grid;grid-template-columns:1fr;gap:12px"> | |
<div class="chart"> | |
<div style="display:flex;justify-content:space-between;align-items:center"> | |
<strong>Sector Contribution (est.)</strong> | |
<small class="muted">Hover segments</small> | |
</div> | |
<svg id="donut" viewBox="0 0 220 220" preserveAspectRatio="xMidYMid meet" style="aspect-ratio:1/1"></svg> | |
<div class="legend" id="donut-legend"></div> | |
</div> | |
<div class="card" style="padding:12px"> | |
<strong>Retail Performance</strong> | |
<p class="muted">Retail sales in Q1 2025 reached 1.708 quadrillion VND (US$66.83B), up 9.9% y/y.</p> | |
<div style="display:flex;gap:12px;align-items:center"> | |
<div style="flex:1"> | |
<div class="progress" aria-hidden><i id="retail-progress" style="width:0"></i></div> | |
<small class="muted">Year-on-year growth vs baseline</small> | |
</div> | |
<div style="width:120px;text-align:right"> | |
<div style="font-weight:800">+9.9%</div> | |
<small class="muted">Q1 2025</small> | |
</div> | |
</div> | |
</div> | |
</div> | |
<details> | |
<summary><strong>Sector notes</strong> <small class="muted" style="margin-left:auto">Expand for details</small></summary> | |
<div style="padding:10px"> | |
<ul> | |
<li>Services: largest contributor to GDP, driven by domestic demand and tourism recovery.</li> | |
<li>Manufacturing: strong export performance and FDI-supported capacity.</li> | |
<li>Banking: projected earnings increase of ~17% due to credit growth and fee income recovery.</li> | |
</ul> | |
</div> | |
</details> | |
</div> | |
</section> | |
<!-- Risks --> | |
<section id="risks" class="card" data-title="Challenges and Risks"> | |
<div class="card-header"> | |
<div> | |
<h2>Challenges & Risk Factors</h2> | |
<div class="meta">External and domestic headwinds to monitor</div> | |
</div> | |
<div class="controls"> | |
<button class="btn ghost" id="showRisks">Highlight</button> | |
</div> | |
</div> | |
<div style="display:grid;gap:8px"> | |
<ul> | |
<li><strong>Global trade tensions:</strong> Pressure on export volumes and supply chains.</li> | |
<li><strong>US tariff policies:</strong> Increased costs for export-oriented businesses.</li> | |
<li><strong>Geopolitical instability:</strong> Heightened uncertainty for investment.</li> | |
<li><strong>FDI concentration:</strong> Risks of overdependence and inflationary pressures.</li> | |
<li><strong>Macroeconomic stability:</strong> Need to balance growth with fiscal prudence.</li> | |
</ul> | |
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap"> | |
<div style="flex:1"> | |
<small class="muted">Risk gauge</small> | |
<div class="progress" aria-hidden style="margin-top:6px"><i id="risk-bar" style="width:40%"></i></div> | |
<small class="muted">Overall risk level: Moderate</small> | |
</div> | |
<div style="min-width:220px"> | |
<small class="muted">Suggested mitigation</small> | |
<ol style="margin:6px 0 0 18px"> | |
<li>Diversify export markets</li> | |
<li>Strengthen domestic demand & fiscal buffers</li> | |
<li>Promote resilient supply chains</li> | |
</ol> | |
</div> | |
</div> | |
</div> | |
</section> | |
<!-- Historical Comparison --> | |
<section id="history" class="card" data-title="Historical Comparison"> | |
<div class="card-header"> | |
<div> | |
<h2>Historical Comparison</h2> | |
<div class="meta">Q1 year-on-year GDP growth: 2020–2025</div> | |
</div> | |
<div class="controls"> | |
<button class="btn ghost" id="toggleSeries">Toggle Series</button> | |
</div> | |
</div> | |
<div class="chart"> | |
<div style="display:flex;justify-content:space-between;align-items:center"> | |
<strong>Q1 GDP Growth (2020–2025)</strong> | |
<small class="muted">Interactive sparkline</small> | |
</div> | |
<svg id="history-line" viewBox="0 0 700 220" preserveAspectRatio="none" style="aspect-ratio: 3.2 / 1;"></svg> | |
<div class="legend" id="history-legend"></div> | |
</div> | |
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap"> | |
<div class="muted">2024 GDP: 7.1% • 2025 forecasts vary (World Bank 5.8%, ADB 6.6%, IMF 5.2%)</div> | |
<div style="margin-left:auto"> | |
<button class="btn" id="savePref">Save view</button> | |
</div> | |
</div> | |
</section> | |
<!-- Outlook --> | |
<section id="outlook" class="card" data-title="Economic Outlook and Projections"> | |
<div class="card-header"> | |
<div> | |
<h2>Outlook & Projections</h2> | |
<div class="meta">Near-term prospects and policy responses</div> | |
</div> | |
<div class="controls"> | |
<span class="chip">Cautiously optimistic</span> | |
</div> | |
</div> | |
<div style="display:grid;gap:10px"> | |
<p class="muted">Vietnam's economy started 2025 strongly but faces external headwinds. Domestic fundamentals—robust FDI, low unemployment and contained inflation—support a resilient near-term outlook. The government's 8%+ target is ambitious relative to international forecasts.</p> | |
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> | |
<div class="card" style="padding:12px"> | |
<strong>Supporting factors</strong> | |
<ul> | |
<li>Robust FDI inflows</li> | |
<li>Low unemployment supporting consumption</li> | |
<li>Controlled inflation</li> | |
</ul> | |
</div> | |
<div class="card" style="padding:12px"> | |
<strong>Risk mitigation policies</strong> | |
<ul> | |
<li>Diversify export markets</li> | |
<li>Strengthen domestic demand</li> | |
<li>Maintain macro stability and fiscal buffers</li> | |
</ul> | |
</div> | |
</div> | |
<details> | |
<summary><strong>Projection scenarios</strong> <small class="muted" style="margin-left:auto">Best/Worse/Base</small></summary> | |
<div style="padding:10px"> | |
<table style="width:100%"> | |
<thead><tr><th>Scenario</th><th>GDP 2025 (est.)</th><th>Key driver</th></tr></thead> | |
<tbody> | |
<tr><td>Optimistic</td><td>8.3–8.5%</td><td>Strong domestic demand + FDI</td></tr> | |
<tr><td>Base</td><td>6.0–7.0%</td><td>Moderate global growth</td></tr> | |
<tr><td>Pessimistic</td><td>4.5–5.5%</td><td>Severe trade disruptions</td></tr> | |
</tbody> | |
</table> | |
</div> | |
</details> | |
</div> | |
</section> | |
<!-- References --> | |
<section id="references" class="card" data-title="References and Citations"> | |
<div class="card-header"> | |
<div> | |
<h2>Sources & Citations</h2> | |
<div class="meta">Primary sources used for data and context</div> | |
</div> | |
<div class="controls"> | |
<button class="btn" id="copyCite">Copy citations</button> | |
</div> | |
</div> | |
<div style="display:grid;gap:8px"> | |
<ol id="sources-list" class="muted"> | |
<li>Trading Economics - Vietnam GDP Annual Growth Rate — https://tradingeconomics.com/vietnam/gdp-growth-annual</li> | |
<li>International Monetary Fund - Vietnam Country Profile — https://www.imf.org/en/Countries/VNM</li> | |
<li>World Economics - Vietnam GDP Estimates — https://www.worldeconomics.com/GDP/Vietnam.gdp</li> | |
<li>Government of Vietnam - General Statistics Office — https://www.gso.gov.vn/en/</li> | |
<li>Wikipedia - Economy of Vietnam — https://en.wikipedia.org/wiki/Economy_of_Vietnam</li> | |
<li>And others (ADB, FocusEconomics, Ministry of Planning and Investment, etc.)</li> | |
</ol> | |
<small class="muted">Click a source to open in a new tab.</small> | |
</div> | |
</section> | |
<footer> | |
<div>Prepared: Vietnam Economic Growth Report 2025 • Interactive Dashboard</div> | |
<div class="muted">Data snapshot updated: June 2025</div> | |
</footer> | |
</main> | |
</div> | |
</div> | |
<script> | |
// Replace feather icons | |
feather.replace(); | |
// Data model | |
const data = { | |
gdpQ1: { years: [2020,2021,2022,2023,2024,2025], values: [3.21,4.85,5.42,3.46,5.98,6.93] }, | |
periodCompare: [ | |
{ label:'Q1 2025', value:6.9, meta:'Actual' }, | |
{ label:'Q2 2025', value:7.96, meta:'Actual' }, | |
{ label:'H1 2025', value:7.52, meta:'Actual' }, | |
], | |
sectors: [ | |
{ name:'Services', value:45, color:'#06b6d4' }, | |
{ name:'Manufacturing', value:30, color:'#0f766e' }, | |
{ name:'Export Industries', value:15, color:'#f59e0b' }, | |
{ name:'Other', value:10, color:'#7c3aed' } | |
], | |
indicators: [ | |
{ indicator:'GDP Q1 2025 (y/y)', value:'6.90%', period:'Q1 2025', source:'GSO' }, | |
{ indicator:'GDP Q2 2025 (y/y)', value:'7.96%', period:'Q2 2025', source:'GSO' }, | |
{ indicator:'H1 2025 GDP (y/y)', value:'7.52%', period:'H1 2025', source:'GSO' }, | |
{ indicator:'Inflation (May 2025)', value:'3.24%', period:'May 2025', source:'GSO/IMF' }, | |
{ indicator:'Inflation (Jun 2025)', value:'3.57%', period:'Jun 2025', source:'GSO/IMF' }, | |
{ indicator:'Unemployment (Q1 2025)', value:'2.20%', period:'Q1 2025', source:'GSO' }, | |
{ indicator:'FDI Registered (first 5 months 2025)', value:'$18.4B', period:'Jan–May 2025', source:'MPI' }, | |
{ indicator:'FDI Disbursed (first 5 months 2025)', value:'$8.9B', period:'Jan–May 2025', source:'MPI' }, | |
{ indicator:'Retail sales Q1 2025', value:'1.708 quadrillion VND', period:'Q1 2025', source:'GSO' }, | |
{ indicator:'Banking sector earnings (2025 est.)', value:'+17%', period:'2025 estimate', source:'Industry' }, | |
] | |
}; | |
// Utility: create elements | |
function el(tag, attrs={}, children=[]){ | |
const e = document.createElement(tag); | |
for(const k in attrs){ | |
if(k==='class') e.className = attrs[k]; | |
else if(k==='html') e.innerHTML = attrs[k]; | |
else e.setAttribute(k, attrs[k]); | |
} | |
(Array.isArray(children)?children:[children]).forEach(c=>{ if(c) e.appendChild(typeof c==='string'?document.createTextNode(c):c) }); | |
return e; | |
} | |
// Populate indicators table | |
const tbody = document.getElementById('indicators-body'); | |
function renderIndicators(list){ | |
tbody.innerHTML = ''; | |
list.forEach(row=>{ | |
const tr = el('tr'); | |
tr.appendChild(el('td',{},[row.indicator])); | |
tr.appendChild(el('td',{},[row.value])); | |
tr.appendChild(el('td',{},[row.period])); | |
tr.appendChild(el('td',{},[row.source])); | |
tbody.appendChild(tr); | |
}); | |
} | |
renderIndicators(data.indicators); | |
// Sorting for table | |
document.querySelectorAll('#indicators-table th').forEach(th=>{ | |
th.addEventListener('click',()=>{ | |
const key = th.getAttribute('data-key'); | |
const mapping = {indicator:'indicator', value:'value', period:'period', source:'source'}; | |
const dir = th.dataset.dir === 'asc' ? 'desc' : 'asc'; | |
th.dataset.dir = dir; | |
const sorted = [...data.indicators].sort((a,b)=>{ | |
let A = a[mapping[key]]; let B = b[mapping[key]]; | |
// normalize numbers | |
if(key==='value'){ | |
const na = parseFloat(A.replace(/[^0-9\.\-]+/g,'')) || 0; | |
const nb = parseFloat(B.replace(/[^0-9\.\-]+/g,'')) || 0; | |
if(dir==='asc') return na-nb; | |
return nb-na; | |
} | |
if(dir==='asc') return A.toString().localeCompare(B.toString()); | |
return B.toString().localeCompare(A.toString()); | |
}); | |
renderIndicators(sorted); | |
}); | |
}); | |
// Export CSV | |
document.getElementById('exportCSV').addEventListener('click', ()=>{ | |
const rows = [['Indicator','Value','Period','Source'], ...data.indicators.map(r=>[r.indicator,r.value,r.period,r.source])]; | |
const csv = rows.map(r=>r.map(cell=>`"${String(cell).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_indicators_2025.csv'; document.body.appendChild(a); a.click(); | |
URL.revokeObjectURL(url); a.remove(); | |
}); | |
// Copy executive summary to clipboard | |
document.getElementById('copyExec').addEventListener('click', async ()=>{ | |
const text = document.getElementById('exec-text').innerText; | |
await navigator.clipboard.writeText(text); | |
flash('Executive summary copied to clipboard'); | |
}); | |
// Copy citations | |
document.getElementById('copyCite').addEventListener('click', async ()=>{ | |
const sources = Array.from(document.querySelectorAll('#sources-list li')).map(li=>li.innerText).join('\n'); | |
await navigator.clipboard.writeText(sources); | |
flash('Citations copied to clipboard'); | |
}); | |
// Quick export button: copies a compact summary + key KPIs | |
document.getElementById('copySummary').addEventListener('click', async ()=>{ | |
const exec = document.getElementById('exec-text').innerText; | |
const kpis = data.indicators.slice(0,6).map(i=>`${i.indicator}: ${i.value}`).join('\n'); | |
const payload = exec + "\n\nKey indicators:\n" + kpis; | |
await navigator.clipboard.writeText(payload); | |
flash('Report summary copied to clipboard'); | |
}); | |
// Flash notification | |
function flash(msg){ | |
const f = el('div',{class:'btn', style:'position:fixed;right:20px;bottom:20px;z-index:999;pointer-events:auto'},[msg]); | |
document.body.appendChild(f); | |
setTimeout(()=>f.style.transform='translateY(-8px)',50); | |
setTimeout(()=>f.remove(),2200); | |
} | |
// Draw simple SVG sparkline for GDP Q1 | |
function drawSparkline(svgEl, series, opts={}){ | |
const w = svgEl.viewBox.baseVal.width || 600; | |
const h = svgEl.viewBox.baseVal.height || 220; | |
const pad = 30; | |
const values = series.values; | |
const years = series.years; | |
const max = Math.max(...values) * 1.12; | |
const min = Math.min(...values) * 0.9; | |
const x = (i)=> pad + (i/(values.length-1))*(w - pad*2); | |
const y = (v)=> pad + ((max - v)/(max-min))*(h - pad*2); | |
// clear | |
svgEl.innerHTML = ''; | |
// grid lines | |
for(let i=0;i<5;i++){ | |
const ly = pad + i*((h-pad*2)/4); | |
const line = document.createElementNS('http://www.w3.org/2000/svg','line'); | |
line.setAttribute('x1',pad); line.setAttribute('x2',w-pad); | |
line.setAttribute('y1',ly); line.setAttribute('y2',ly); | |
line.setAttribute('stroke','#eef2f7'); line.setAttribute('stroke-width','1'); | |
svgEl.appendChild(line); | |
} | |
// polyline | |
const pts = values.map((v,i)=>`${x(i)},${y(v)}`).join(' '); | |
const poly = document.createElementNS('http://www.w3.org/2000/svg','polyline'); | |
poly.setAttribute('points', pts); | |
poly.setAttribute('fill','none'); | |
poly.setAttribute('stroke','#0f766e'); | |
poly.setAttribute('stroke-width','3'); | |
poly.setAttribute('stroke-linecap','round'); | |
poly.setAttribute('stroke-linejoin','round'); | |
svgEl.appendChild(poly); | |
// area fill | |
const areaPts = pts + ` ${w-pad},${h-pad} ${pad},${h-pad}`; | |
const area = document.createElementNS('http://www.w3.org/2000/svg','path'); | |
const d = `M ${values.map((v,i)=>`${x(i)} ${y(v)}`).join(' L ')} L ${w-pad} ${h-pad} L ${pad} ${h-pad} Z`; | |
area.setAttribute('d', d); | |
area.setAttribute('fill','rgba(6,182,212,0.08)'); | |
svgEl.appendChild(area); | |
// points + interactive tooltip | |
const tooltip = el('div',{style:'position:absolute;display:none;padding:8px;background:#fff;border-radius:8px;box-shadow:var(--shadow);font-size:0.9rem;color:#0f172a;'}); | |
document.body.appendChild(tooltip); | |
values.forEach((v,i)=>{ | |
const cx = x(i), cy = y(v); | |
const c = document.createElementNS('http://www.w3.org/2000/svg','circle'); | |
c.setAttribute('cx',cx); c.setAttribute('cy',cy); c.setAttribute('r',4); | |
c.setAttribute('fill','#06b6d4'); c.setAttribute('stroke','#fff'); c.setAttribute('stroke-width','1.5'); | |
c.style.cursor='pointer'; | |
svgEl.appendChild(c); | |
c.addEventListener('mouseenter',(ev)=>{ | |
tooltip.style.display='block'; | |
tooltip.innerHTML = `<strong>${years[i]}</strong><div class="muted">${v}%</div>`; | |
}); | |
c.addEventListener('mouseleave',()=> tooltip.style.display='none'); | |
c.addEventListener('mousemove',(ev)=>{ | |
tooltip.style.left = (ev.pageX + 12) + 'px'; | |
tooltip.style.top = (ev.pageY - 18) + 'px'; | |
}); | |
}); | |
// legend | |
const lg = document.getElementById('gdp-legend'); | |
lg.innerHTML = `<div style="display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;background:#0f766e;border-radius:4px;display:inline-block"></span><small class="muted">Q1 Growth</small></div>`; | |
} | |
// Draw period bars | |
function drawPeriodBars(svgEl, series){ | |
const w = svgEl.viewBox.baseVal.width || 400; | |
const h = svgEl.viewBox.baseVal.height || 220; | |
const pad = 30; | |
const barW = (w - pad*2) / (series.length * 1.8); | |
const max = Math.max(...series.map(s=>s.value))*1.15; | |
svgEl.innerHTML = ''; | |
series.forEach((s,i)=>{ | |
const bx = pad + i*(barW*1.8); | |
const bh = ((s.value)/max)*(h - pad*2); | |
const by = h - pad - bh; | |
const rect = document.createElementNS('http://www.w3.org/2000/svg','rect'); | |
rect.setAttribute('x',bx); rect.setAttribute('y',by); | |
rect.setAttribute('width',barW); rect.setAttribute('height',bh); | |
rect.setAttribute('fill','#06b6d4'); rect.setAttribute('rx',8); | |
rect.style.cursor='pointer'; | |
svgEl.appendChild(rect); | |
// label | |
const lbl = document.createElementNS('http://www.w3.org/2000/svg','text'); | |
lbl.setAttribute('x',bx + barW/2); lbl.setAttribute('y',h - pad + 18); | |
lbl.setAttribute('text-anchor','middle'); lbl.setAttribute('fill','#394155'); lbl.setAttribute('font-size','12'); | |
lbl.textContent = s.label; | |
svgEl.appendChild(lbl); | |
// value text | |
const vt = document.createElementNS('http://www.w3.org/2000/svg','text'); | |
vt.setAttribute('x',bx + barW/2); vt.setAttribute('y',by - 8); | |
vt.setAttribute('text-anchor','middle'); vt.setAttribute('fill','#0f172a'); vt.setAttribute('font-weight','700'); | |
vt.textContent = s.value + '%'; | |
svgEl.appendChild(vt); | |
rect.addEventListener('click', ()=> { | |
// filter table to show matching period | |
const filtered = data.indicators.filter(it => it.period.includes(s.label.split(' ')[0]) || it.indicator.includes(s.label.split(' ')[0])); | |
if(filtered.length) renderIndicators(filtered); | |
flash('Filtered indicators by ' + s.label); | |
}); | |
}); | |
// reset button | |
document.getElementById('showAll').addEventListener('click', ()=> { | |
renderIndicators(data.indicators); | |
}); | |
} | |
// Draw donut chart | |
function drawDonut(svgEl, sectors){ | |
const w = svgEl.viewBox.baseVal.width || 220; | |
const h = svgEl.viewBox.baseVal.height || 220; | |
const cx = w/2, cy = h/2; | |
const radius = Math.min(w,h)/2 - 16; | |
const total = sectors.reduce((s,c)=>s+c.value,0); | |
let angle = -Math.PI/2; | |
svgEl.innerHTML = ''; | |
sectors.forEach((s,i)=>{ | |
const slice = (s.value/total) * Math.PI*2; | |
const x1 = cx + Math.cos(angle) * (radius); | |
const y1 = cy + Math.sin(angle) * (radius); | |
const angle2 = angle + slice; | |
const x2 = cx + Math.cos(angle2) * (radius); | |
const y2 = cy + Math.sin(angle2) * (radius); | |
const large = slice > Math.PI ? 1 : 0; | |
const path = document.createElementNS('http://www.w3.org/2000/svg','path'); | |
const d = `M ${cx} ${cy} L ${x1} ${y1} A ${radius} ${radius} 0 ${large} 1 ${x2} ${y2} Z`; | |
path.setAttribute('d',d); | |
path.setAttribute('fill', s.color); | |
path.style.cursor='pointer'; | |
svgEl.appendChild(path); | |
// add small interaction | |
path.addEventListener('mouseenter', (ev)=>{ | |
const tip = document.getElementById('donut-tip') || null; | |
showTooltip(ev.pageX, ev.pageY, `${s.name}: ${s.value}%`); | |
}); | |
path.addEventListener('mouseleave', hideTooltip); | |
angle += slice; | |
}); | |
// hole | |
const hole = document.createElementNS('http://www.w3.org/2000/svg','circle'); | |
hole.setAttribute('cx',cx); hole.setAttribute('cy',cy); hole.setAttribute('r',radius*0.5); | |
hole.setAttribute('fill','#fff'); | |
svgEl.appendChild(hole); | |
// legend | |
const legend = document.getElementById('donut-legend'); | |
legend.innerHTML = ''; | |
sectors.forEach(s=> { | |
const item = el('div',{},[ | |
el('span',{style:`width:12px;height:12px;background:${s.color};display:inline-block;border-radius:4px;margin-right:6px`}), | |
el('small',{class:'muted'},`${s.name} ${s.value}%`) | |
]); | |
legend.appendChild(item); | |
}); | |
} | |
// Simple global tooltip | |
const globalTip = el('div',{id:'globalTip', style:'position:fixed;display:none;padding:8px;background:white;border-radius:8px;box-shadow:var(--shadow);font-size:0.9rem;pointer-events:none;z-index:999'}); | |
document.body.appendChild(globalTip); | |
function showTooltip(x,y,html){ | |
globalTip.style.left = (x+12)+'px'; | |
globalTip.style.top = (y-28)+'px'; | |
globalTip.innerHTML = html; | |
globalTip.style.display = 'block'; | |
} | |
function hideTooltip(){ globalTip.style.display = 'none'; } | |
// Draw history line | |
function drawHistory(svgEl, series){ | |
const w = svgEl.viewBox.baseVal.width || 700; | |
const h = svgEl.viewBox.baseVal.height || 220; | |
const pad = 40; | |
const values = series.values; | |
const years = series.years; | |
const max = Math.max(...values) * 1.12; | |
const min = Math.min(...values) * 0.9; | |
const x = (i)=> pad + (i/(values.length-1))*(w - pad*2); | |
const y = (v)=> pad + ((max - v)/(max-min))*(h - pad*2); | |
svgEl.innerHTML = ''; | |
// axes labels | |
years.forEach((yr,i)=>{ | |
const tx = document.createElementNS('http://www.w3.org/2000/svg','text'); | |
tx.setAttribute('x', x(i)); | |
tx.setAttribute('y', h - 10); | |
tx.setAttribute('text-anchor','middle'); | |
tx.setAttribute('fill','#64748b'); | |
tx.setAttribute('font-size','12'); | |
tx.textContent = yr; | |
svgEl.appendChild(tx); | |
}); | |
// polyline | |
const pts = values.map((v,i)=>`${x(i)},${y(v)}`).join(' '); | |
const poly = document.createElementNS('http://www.w3.org/2000/svg','polyline'); | |
poly.setAttribute('points', pts); | |
poly.setAttribute('fill','none'); | |
poly.setAttribute('stroke','#7c3aed'); | |
poly.setAttribute('stroke-width','3'); | |
poly.setAttribute('stroke-linecap','round'); | |
svgEl.appendChild(poly); | |
// points + interactions | |
values.forEach((v,i)=>{ | |
const cx = x(i), cy = y(v); | |
const c = document.createElementNS('http://www.w3.org/2000/svg','circle'); | |
c.setAttribute('cx',cx); c.setAttribute('cy',cy); c.setAttribute('r',5); | |
c.setAttribute('fill','#7c3aed'); c.setAttribute('stroke','#fff'); c.setAttribute('stroke-width','1.5'); | |
svgEl.appendChild(c); | |
c.addEventListener('mouseenter', (ev)=> showTooltip(ev.pageX, ev.pageY, `<strong>${years[i]}</strong><div class="muted">${v}%</div>`)); | |
c.addEventListener('mouseleave', hideTooltip); | |
}); | |
const lg = document.getElementById('history-legend'); | |
lg.innerHTML = `<div style="display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;background:#7c3aed;border-radius:4px;display:inline-block"></span><small class="muted">Q1 y/y</small></div>`; | |
} | |
// Initialize visuals | |
drawSparkline(document.getElementById('gdp-sparkline'), data.gdpQ1); | |
drawPeriodBars(document.getElementById('period-bars'), data.periodCompare); | |
drawDonut(document.getElementById('donut'), data.sectors); | |
drawHistory(document.getElementById('history-line'), data.gdpQ1); | |
// Retail progress fill | |
document.getElementById('retail-progress').style.width = '36%'; | |
// Interactive controls | |
document.getElementById('toggleForecasts').addEventListener('click', ()=>{ | |
const body = document.getElementById('indicators-body'); | |
// toggle show forecasts rows (simulate) | |
if(body.dataset.showing==='forecasts'){ | |
renderIndicators(data.indicators); | |
body.dataset.showing=''; | |
} else { | |
const extra = [ | |
{ indicator:'World Bank 2025 GDP forecast', value:'5.8%', period:'2025 forecast', source:'World Bank' }, | |
{ indicator:'ADB 2025 GDP forecast', value:'6.6%', period:'2025 forecast', source:'ADB' }, | |
{ indicator:'IMF 2025 GDP forecast', value:'5.2%', period:'2025 forecast', source:'IMF' }, | |
]; | |
renderIndicators([...data.indicators, ...extra]); | |
body.dataset.showing='forecasts'; | |
} | |
}); | |
// TOC smooth scroll & active highlight via IntersectionObserver | |
document.querySelectorAll('.toc-link').forEach(a=>{ | |
a.addEventListener('click', (ev)=>{ | |
ev.preventDefault(); | |
const id = a.dataset.target; | |
document.getElementById(id).scrollIntoView({behavior:'smooth', block:'start'}); | |
}); | |
}); | |
const sections = document.querySelectorAll('main.report section'); | |
const tocLinks = document.querySelectorAll('.toc-link'); | |
const obs = new IntersectionObserver((entries)=>{ | |
entries.forEach(entry=>{ | |
if(entry.isIntersecting){ | |
const id = entry.target.id; | |
tocLinks.forEach(a=>a.classList.toggle('active', a.dataset.target===id)); | |
} | |
}); | |
}, {root: null, rootMargin: '-30% 0px -55% 0px', threshold: 0}); | |
sections.forEach(s=>obs.observe(s)); | |
// Reading progress bar | |
const progressBar = document.getElementById('progress-bar'); | |
window.addEventListener('scroll', ()=>{ | |
const doc = document.documentElement; | |
const total = doc.scrollHeight - doc.clientHeight; | |
const pct = (window.scrollY / total) * 100; | |
progressBar.style.width = pct + '%'; | |
}, {passive:true}); | |
// Search/filter across sections (highlights matching sections & table) | |
const globalSearch = document.getElementById('global-search'); | |
globalSearch.addEventListener('input', (e)=>{ | |
const q = e.target.value.trim().toLowerCase(); | |
if(!q){ sections.forEach(s=>s.style.display=''); renderIndicators(data.indicators); return; } | |
sections.forEach(s=>{ | |
const txt = s.innerText.toLowerCase(); | |
s.style.display = txt.includes(q) ? '' : 'none'; | |
}); | |
// filter indicators table | |
const filtered = data.indicators.filter(i=> (i.indicator + ' ' + i.value + ' ' + i.period + ' ' + i.source).toLowerCase().includes(q)); | |
renderIndicators(filtered); | |
}); | |
// Save view preferences in localStorage | |
document.getElementById('savePref').addEventListener('click', ()=>{ | |
const prefs = {savedAt: Date.now(), view:'default'}; | |
localStorage.setItem('reportPrefs', JSON.stringify(prefs)); | |
document.getElementById('savedFilters').innerText = 'Saved'; | |
flash('View saved'); | |
}); | |
// Restore saved prefs label | |
const prefs = JSON.parse(localStorage.getItem('reportPrefs') || 'null'); | |
if(prefs) document.getElementById('savedFilters').innerText = 'Saved'; | |
// Highlight risks on button click | |
document.getElementById('showRisks').addEventListener('click', ()=>{ | |
const sec = document.getElementById('risks'); | |
sec.animate([{boxShadow:'none'},{boxShadow:'0 0 0 6px rgba(239,68,68,0.08)'}],{duration:600,iterations:1}); | |
sec.scrollIntoView({behavior:'smooth'}); | |
flash('Risks highlighted'); | |
}); | |
// Tooltip for donut implemented via global show/hide | |
document.addEventListener('mousemove', ()=>{ /* keep available for chart events */ }); | |
// Small Intersection observer to animate KPI values (count up) | |
const kpiObserver = new IntersectionObserver((entries)=>{ | |
entries.forEach(entry=>{ | |
if(entry.isIntersecting){ | |
const els = entry.target.querySelectorAll('.kpi .value'); | |
els.forEach(v=>{ | |
if(v.dataset.animated) return; | |
v.dataset.animated = '1'; | |
// animate numbers if numeric | |
const num = parseFloat(v.innerText); | |
if(!isNaN(num)) { | |
const start = Math.max(0, num*0.6); | |
const end = num; | |
let cur = start; | |
const diff = end - start; | |
const dur = 800; | |
const step = 16; | |
const steps = dur/step; | |
let i=0; | |
const iv = setInterval(()=>{ | |
i++; | |
const val = start + (diff*(i/steps)); | |
v.innerText = (Math.round(val*100)/100) + (v.innerText.includes('%')? '%' : ''); | |
if(i>=steps) clearInterval(iv); | |
}, step); | |
} | |
}); | |
kpiObserver.unobserve(entry.target); | |
} | |
}); | |
}, {threshold:0.2}); | |
document.querySelectorAll('section.card').forEach(s=>kpiObserver.observe(s)); | |
// Table row click -> show contextual section | |
tbody.addEventListener('click', (ev)=>{ | |
const tr = ev.target.closest('tr'); | |
if(!tr) return; | |
const indicator = tr.children[0].innerText; | |
// simple mapping | |
if(indicator.toLowerCase().includes('fdi')) document.getElementById('sector').scrollIntoView({behavior:'smooth'}); | |
else if(indicator.toLowerCase().includes('inflation')) document.getElementById('risks').scrollIntoView({behavior:'smooth'}); | |
else document.getElementById('indicators').scrollIntoView({behavior:'smooth'}); | |
}); | |
// Misc: keyboard shortcut "/" focuses search | |
window.addEventListener('keydown', (e)=>{ | |
if(e.key === '/') { e.preventDefault(); globalSearch.focus(); } | |
}); | |
// Small performance: lazy-load heavy visuals on intersection | |
const lazyObs = new IntersectionObserver((entries, observer)=>{ | |
entries.forEach(ent=>{ | |
if(ent.isIntersecting){ | |
// On first view, animate period bars slightly | |
const bars = document.getElementById('period-bars'); | |
bars.querySelectorAll('rect').forEach((r,i)=>{ | |
r.style.transformOrigin = 'center bottom'; | |
r.animate([{transform:'scaleY(0)'},{transform:'scaleY(1)'}], {duration:400 + i*90, easing:'cubic-bezier(.16,1,.3,1)', fill:'forwards'}); | |
}); | |
observer.unobserve(ent.target); | |
} | |
}); | |
}, {root:null, rootMargin:'0px', threshold:0.18}); | |
lazyObs.observe(document.getElementById('indicators')); | |
// Accessibility: add target=_blank for source links (in references) | |
document.querySelectorAll('#sources-list li').forEach(li=>{ | |
const text = li.innerText; | |
const m = text.match(/https?:\/\/\S+/); | |
if(m){ | |
const a = document.createElement('a'); a.href = m[0]; a.target='_blank'; a.rel='noopener'; | |
a.innerText = m[0]; | |
li.innerHTML = li.innerHTML.replace(m[0], ''); | |
li.appendChild(a); | |
} | |
}); | |
// Small responsive: toggle filters panel for mobile (just scroll to sector) | |
document.getElementById('toggleFilters').addEventListener('click', ()=> { | |
document.getElementById('sector').scrollIntoView({behavior:'smooth'}); | |
}); | |
// Provide clean initial focus | |
document.getElementById('app').focus(); | |
</script> | |
</body> | |
</html> |