gpt5-mini-v5 / index.html
thucdangvan020999's picture
Upload index.html with huggingface_hub
86d223f 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 Dashboard</title>
<!-- Fonts & Icons -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-w3q7F3rj7q8qA1m3lKX8pR4pQGm6g5YV0w1N5aKxJr9b3p2Y6s8Zs5R6d3j4k1v2bW8z5c6d9f0g3h5k2j1g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
:root{
--bg:#0f1724;
--card:#0b1220;
--muted:#9aa4b2;
--accent:#0ea5a4;
--accent-2:#7c3aed;
--glass: rgba(255,255,255,0.03);
--success:#10b981;
--danger:#ef4444;
--gap: clamp(0.5rem, 0.9vw, 1.25rem);
--radius: 12px;
color-scheme: dark;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial;
background:linear-gradient(180deg,#071224 0%, #071422 45%, #04101b 100%);
color: #e6eef6;
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
padding: clamp(0.5rem,1.5vw,1.5rem);
display:flex;
flex-direction:column;
gap:var(--gap);
min-height:100%;
}
/* Layout */
.app{
display:grid;
grid-template-columns: 1fr;
gap:var(--gap);
align-items:start;
width:100%;
max-width:1400px;
margin:0 auto;
}
header{
background:linear-gradient(90deg, rgba(255,255,255,0.02), transparent);
border-radius:var(--radius);
padding: clamp(0.75rem,1.6vw,1.4rem);
display:flex;
gap:var(--gap);
align-items:center;
position:relative;
overflow:hidden;
}
.brand{
display:flex;
gap:0.75rem;
align-items:center;
min-width:0;
}
.logo{
width:56px;height:56px;border-radius:12px;
background:linear-gradient(135deg,var(--accent),var(--accent-2));
display:flex;align-items:center;justify-content:center;
font-weight:800;font-size:20px;
box-shadow: 0 6px 18px rgba(15,120,120,0.15);
}
.title{
min-width:0;
}
.title h1{
margin:0;font-size:clamp(1rem,2.3vw,1.45rem);letter-spacing:-0.3px;
font-weight:700;
}
.title p{margin:0;color:var(--muted);font-size:0.85rem}
/* Controls */
.controls{margin-left:auto;display:flex;gap:0.5rem;align-items:center}
.search{
display:flex;align-items:center;gap:0.5rem;background:var(--glass);
padding:6px 10px;border-radius:10px;color:var(--muted);
width:220px;
}
.search input{background:transparent;border:0;outline:none;color:inherit;font-size:0.95rem;width:100%}
.btn{
background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
border:1px solid rgba(255,255,255,0.04);
padding:8px 10px;border-radius:10px;color:inherit;cursor:pointer;
display:inline-flex;gap:0.5rem;align-items:center;font-weight:600;font-size:0.9rem;
}
.btn.ghost{background:transparent;border:1px dashed rgba(255,255,255,0.03)}
.btn.primary{background:linear-gradient(90deg,var(--accent),var(--accent-2)); color:#04111a;border:0}
/* Main grid: TOC + content */
.main{
display:grid;
grid-template-columns: 1fr;
gap:var(--gap);
}
/* Sticky TOC for wide screens */
.layout{
display:grid;
gap:var(--gap);
grid-template-columns: 300px 1fr;
}
.toc{
position:sticky;top:calc(1rem + 16px);align-self:start;
background:linear-gradient(180deg, rgba(255,255,255,0.01), transparent);
border-radius:var(--radius);
padding:var(--gap);
min-height:200px;
display:flex;flex-direction:column;gap:0.75rem;
font-size:0.95rem;color:var(--muted);
}
.toc h3{margin:0 0 0.25rem 0;font-size:0.95rem;color:#d9eef6}
.toc nav{display:flex;flex-direction:column;gap:0.25rem}
.toc a{
color:var(--muted);text-decoration:none;padding:6px 8px;border-radius:8px;display:flex;gap:8px;align-items:center;
}
.toc a.active, .toc a:hover{background:rgba(255,255,255,0.02);color:var(--accent);font-weight:600}
/* Content */
.content{
display:grid;gap:var(--gap);
}
/* KPI strip */
.kpis{
display:grid;grid-template-columns:repeat(2,1fr);gap:var(--gap);
}
.kpi{
background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
padding:var(--gap);border-radius:var(--radius);display:flex;align-items:center;gap:0.75rem;
min-height:84px;
}
.kpi .icon{width:52px;height:52px;border-radius:10px;background:rgba(255,255,255,0.02);display:flex;align-items:center;justify-content:center;font-size:1.2rem}
.kpi h4{margin:0;font-size:1.05rem}
.kpi p{margin:0;color:var(--muted);font-size:0.9rem}
.kpi-grid{
display:grid;grid-template-columns:repeat(4,1fr);gap:var(--gap);
}
/* Cards */
.card{
background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
padding:var(--gap);border-radius:var(--radius);overflow:hidden;
}
.card h3{margin:0 0 0.5rem 0;font-size:1rem}
.card .meta{color:var(--muted);font-size:0.9rem;margin-bottom:0.6rem}
/* Charts area */
.charts{
display:grid;
grid-template-columns: 1fr;
gap:var(--gap);
}
.chart-row{
display:grid;
grid-template-columns: 2fr 1fr;
gap:var(--gap);
}
.mini-charts{
display:grid;grid-template-columns:repeat(3,1fr);gap:var(--gap);
}
/* Table */
table{width:100%;border-collapse:collapse;font-size:0.95rem}
th,td{padding:10px 12px;text-align:left;border-bottom:1px dashed rgba(255,255,255,0.03)}
th{color:var(--muted);font-weight:700;cursor:pointer}
tr:hover td{background:rgba(255,255,255,0.01)}
/* Collapsible */
.collapsible{border-radius:10px;overflow:hidden}
.collapsible summary{
list-style:none;cursor:pointer;padding:10px;background:rgba(255,255,255,0.01);display:flex;justify-content:space-between;align-items:center;border:1px solid rgba(255,255,255,0.02)
}
.collapsible summary::-webkit-details-marker{display:none}
/* References */
.sources{display:grid;gap:8px;font-size:0.92rem;color:var(--muted)}
/* Progress & footer */
.progress-wrap{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:60;display:flex;gap:8px;align-items:center}
.progress{width:260px;height:8px;background:rgba(255,255,255,0.03);border-radius:999px;overflow:hidden}
.progress > i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--accent-2));width:0%}
footer{padding:10px 12px;border-radius:12px;background:transparent;color:var(--muted);display:flex;justify-content:space-between;align-items:center;font-size:0.9rem}
/* Tooltips */
.tooltip{
position:fixed;pointer-events:none;padding:8px 10px;background:rgba(10,14,18,0.95);border:1px solid rgba(255,255,255,0.04);
color:#dff6f5;border-radius:8px;font-size:0.9rem;transform:translate(-50%,-120%);white-space:nowrap;z-index:80;opacity:0;transition:opacity .15s ease;
}
/* Small screens adjustments */
@media (max-width:1024px){
.layout{grid-template-columns: 1fr}
.toc{position:relative;top:0;order:2}
.chart-row{grid-template-columns:1fr}
.mini-charts{grid-template-columns:repeat(2,1fr)}
.kpi-grid{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:768px){
header{flex-direction:column;align-items:flex-start}
.controls{width:100%;justify-content:space-between}
.search{width:100%}
.kpi-grid{grid-template-columns:1fr}
.mini-charts{grid-template-columns:1fr}
}
/* Container queries */
.card[style]{
container-type: inline-size;
}
@container (min-width:420px){
.card h3{font-size:1.05rem}
}
/* Utility */
.muted{color:var(--muted)}
.tag{padding:6px 8px;border-radius:999px;background:rgba(255,255,255,0.03);font-size:0.85rem;color:var(--muted)}
.positive{color:var(--success);font-weight:700}
.negative{color:var(--danger);font-weight:700}
.small{font-size:0.85rem;color:var(--muted)}
.legend{display:flex;gap:0.5rem;flex-wrap:wrap}
.legend button{border:0;background:rgba(255,255,255,0.02);padding:6px 8px;border-radius:8px;color:var(--muted);cursor:pointer}
</style>
</head>
<body>
<div class="app" id="app">
<header>
<div class="brand">
<div class="logo" aria-hidden>VN</div>
<div class="title">
<h1>Vietnam Economic Growth Report β€” 2025</h1>
<p>Executive analytics dashboard β€” KPIs, trends, sectoral insights & risk analysis</p>
</div>
</div>
<div class="controls">
<div class="search" title="Search in report">
<i class="fa fa-magnifying-glass"></i>
<input id="globalSearch" placeholder="Search report..." aria-label="Search report" />
</div>
<button class="btn ghost" id="toggleTheme" title="Copy executive summary"><i class="fa fa-sun"></i></button>
<button class="btn" id="copySummary" title="Copy executive summary"><i class="fa fa-copy"></i> Copy</button>
<button class="btn primary" id="exportCsv" title="Export indicators CSV"><i class="fa fa-file-export"></i> Export</button>
</div>
</header>
<main class="main">
<section class="layout">
<aside class="toc" id="toc">
<h3>Contents</h3>
<nav>
<a href="#executive" data-target="executive" class="active"><i class="fa fa-star"></i> Executive Summary</a>
<a href="#kpis" data-target="kpis"><i class="fa fa-tachometer-alt"></i> Key Indicators</a>
<a href="#charts" data-target="charts"><i class="fa fa-chart-line"></i> Charts & Trends</a>
<a href="#sector" data-target="sector"><i class="fa fa-industry"></i> Sectoral Analysis</a>
<a href="#risks" data-target="risks"><i class="fa fa-exclamation-triangle"></i> Risks & Mitigation</a>
<a href="#methodology" data-target="methodology"><i class="fa fa-cogs"></i> Methodology</a>
<a href="#references" data-target="references"><i class="fa fa-book"></i> Sources</a>
</nav>
<div style="margin-top:8px">
<div class="small muted">Jump to</div>
<div style="display:flex;gap:6px;margin-top:6px">
<button class="btn ghost" id="backTop"><i class="fa fa-arrow-up"></i></button>
<button class="btn ghost" id="saveView"><i class="fa fa-save"></i></button>
</div>
</div>
</aside>
<section class="content" id="content">
<article id="executive" class="card">
<h3>Executive Summary</h3>
<div class="meta">High-level synthesis and context β€” 2025 performance highlights</div>
<div style="display:flex;gap:var(--gap);flex-direction:column">
<p id="execText" class="small" style="line-height:1.5">
Vietnam's economy continues to demonstrate robust growth momentum in 2025, with GDP expanding 7.96% year-on-year in Q2 2025 and recording 7.52% growth for H1 2025 β€” the strongest mid-year result since 2011. Industry and services lead growth despite global trade tensions. Core fundamentals such as low unemployment, controlled inflation, and strong FDI inflows underpin near-term prospects, while external risks (tariffs, geopolitical instability) could moderate outcomes. Government targets remain ambitious relative to multilateral forecasts.
</p>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<button class="btn" id="copyExec"><i class="fa fa-clipboard"></i> Copy Summary</button>
<button class="btn ghost" id="downloadJson"><i class="fa fa-download"></i> Download JSON</button>
<button class="btn" id="expandExec"><i class="fa fa-chevron-down"></i> Expand</button>
</div>
</div>
</article>
<section id="kpis" class="card">
<h3>Key Performance Indicators (KPIs)</h3>
<div class="meta">Snapshot of main metrics β€” click to explore</div>
<div class="kpi-grid" style="margin-top:var(--gap)">
<div class="kpi" data-kpi="gdp">
<div class="icon"><i class="fa fa-chart-simple"></i></div>
<div>
<h4 id="gdpNow">GDP H1 2025: 7.52%</h4>
<p class="small">Q2 YoY: 7.96% β€’ Q1: 6.9% β€’ Government Target: 8.3–8.5%</p>
</div>
</div>
<div class="kpi" data-kpi="inflation">
<div class="icon"><i class="fa fa-percent"></i></div>
<div>
<h4 id="inflNow">Inflation June 2025: 3.57%</h4>
<p class="small">IMF: 2.9% β€’ ADB: 4.0% β€” within 3–4.5% target</p>
</div>
</div>
<div class="kpi" data-kpi="unemp">
<div class="icon"><i class="fa fa-briefcase"></i></div>
<div>
<h4 id="unempNow">Unemployment Q1 2025: 2.20%</h4>
<p class="small">Stable, historically low labor-market figure</p>
</div>
</div>
<div class="kpi" data-kpi="fdi">
<div class="icon"><i class="fa fa-hand-holding-dollar"></i></div>
<div>
<h4 id="fdiNow">FDI H1 2025: US$21.51B (+32.6% YoY)</h4>
<p class="small">Registered capital (5 months): $18.4B; Disbursed: $8.9B</p>
</div>
</div>
</div>
</section>
<section id="charts" class="card">
<h3>Charts & Trends</h3>
<div class="meta">Interactive visualizations β€” hover, toggle and filter</div>
<div class="charts" style="margin-top:var(--gap)">
<div class="chart-row">
<div class="card" id="gdpChartCard">
<h3>GDP Growth β€” Q1 (2020–2025) & H1 2025</h3>
<div class="small muted">Year-on-year (%) β€” historical and short-run trend</div>
<div id="gdpChart" style="height:260px;margin-top:12px"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px">
<div class="legend" id="gdpLegend"></div>
<div>
<button class="btn ghost" id="toggleSmooth">Toggle smoothing</button>
<button class="btn" id="copyChartSvg"><i class="fa fa-clone"></i></button>
</div>
</div>
</div>
<div class="card">
<h3>Forecast Comparison 2025</h3>
<div class="small muted">World Bank, ADB, IMF, Government</div>
<div id="forecastBar" style="height:260px;margin-top:10px"></div>
<div style="margin-top:10px" class="small muted">Click legend to highlight</div>
</div>
</div>
<div class="mini-charts" style="margin-top:var(--gap)">
<div class="card">
<h3>Inflation (May–June 2025)</h3>
<div class="small muted">Monthly CPI (%)</div>
<div id="inflationSpark" style="height:140px;margin-top:12px"></div>
</div>
<div class="card">
<h3>FDI Inflows β€” H1 2025</h3>
<div class="small muted">Registered vs Disbursed (US$B)</div>
<div id="fdiDonut" style="height:140px;margin-top:12px"></div>
</div>
<div class="card">
<h3>Retail Sales Q1 2025</h3>
<div class="small muted">1.708 quadrillion VND β€” YoY growth 9.9%</div>
<div style="margin-top:12px">
<div class="tag">Consumption-led recovery</div>
</div>
</div>
</div>
</div>
</section>
<section id="sector" class="card">
<h3>Sectoral Analysis</h3>
<div class="meta">Drivers, traction and structural context</div>
<div style="display:grid;grid-template-columns:1fr;gap:var(--gap);margin-top:var(--gap)">
<div class="collapsible">
<details open>
<summary>
<div style="display:flex;gap:10px;align-items:center">
<strong>Primary Growth Drivers</strong>
<div class="small muted">Services, Manufacturing, Exports, Banking</div>
</div>
<i class="fa fa-chevron-down"></i>
</summary>
<div style="padding:12px">
<ul class="small muted">
<li><strong>Services:</strong> Leading contributor to GDP growth in H1 2025.</li>
<li><strong>Manufacturing:</strong> Recovery supported by foreign investment and export demand.</li>
<li><strong>Exports:</strong> Remain the backbone despite trade tensions.</li>
<li><strong>Banking:</strong> Projected +17% earnings growth in 2025 based on credit growth of 15%.</li>
</ul>
<div style="margin-top:10px">
<button class="btn ghost" id="drillSector">Drill into sectors</button>
</div>
</div>
</details>
</div>
<div class="card">
<h3>Comparative Industry Snapshot</h3>
<div class="small muted">Normalized contributions & narrative insights</div>
<div style="margin-top:12px" id="sectorBars" style="height:160px"></div>
<div style="margin-top:8px" class="small muted">Hover bars for details β€’ Click to pin insight</div>
</div>
</div>
</section>
<section id="risks" class="card">
<h3>Challenges & Risk Factors</h3>
<div class="meta">Scenario-aware planning and mitigation options</div>
<ul class="small muted" style="margin-top:12px">
<li><strong>Global trade tensions:</strong> Potential export slowdowns.</li>
<li><strong>US tariff policies:</strong> Pressure on export-oriented firms.</li>
<li><strong>Geopolitics:</strong> Elevated uncertainty for supply chains.</li>
<li><strong>FDI concentration:</strong> Overdependence risks and inflationary pressure.</li>
<li><strong>Macroeconomic trade-offs:</strong> Growth vs. inflation and public debt.</li>
</ul>
<div style="display:flex;gap:8px;margin-top:12px">
<button class="btn" id="mitigateBtn"><i class="fa fa-lightbulb"></i> Mitigation strategies</button>
<button class="btn ghost" id="scenarioBtn"><i class="fa fa-sliders-h"></i> Run scenario</button>
</div>
</section>
<section id="methodology" class="card">
<h3>Methodology</h3>
<div class="meta">Data processing, assumptions & limitations</div>
<ol class="small muted" style="margin-top:12px">
<li>Data aggregated from cited sources (GSO, IMF, ADB, Trading Economics).</li>
<li>YoY calculations based on official quarterly releases; forecasts compared across institutions.</li>
<li>Charts use normalized scaling for cross-metric comparison; interactivity does not alter source raw values.</li>
<li>Limitations: near-real-time revisions possible; cross-check with primary sources recommended.</li>
</ol>
</section>
<section id="references" class="card">
<h3>Sources & Citations</h3>
<div class="meta">Primary sources used to compile this dashboard</div>
<div class="sources" style="margin-top:12px">
<a href="https://tradingeconomics.com/vietnam/gdp-growth-annual" target="_blank">Trading Economics β€” Vietnam GDP</a>
<a href="https://www.imf.org/en/Countries/VNM" target="_blank">IMF β€” Vietnam Country Profile</a>
<a href="https://www.gso.gov.vn/en/" target="_blank">General Statistics Office of Vietnam</a>
<a href="https://www.adb.org/countries/viet-nam/main" target="_blank">Asian Development Bank β€” Vietnam</a>
<a href="https://vneconomictimes.com/" target="_blank">Vietnam Economic Times</a>
</div>
</section>
<section id="appendices" class="card">
<h3>Appendices</h3>
<div class="meta">Extended tables & raw figures</div>
<div style="margin-top:12px;overflow:auto">
<table id="dataTable">
<thead>
<tr>
<th data-key="metric">Metric</th>
<th data-key="q1">Q1 2025</th>
<th data-key="q2">Q2 2025</th>
<th data-key="h1">H1 2025</th>
<th data-key="note">Note</th>
</tr>
</thead>
<tbody>
<tr>
<td>GDP Growth (YoY)</td>
<td>6.9%</td>
<td>7.96%</td>
<td>7.52%</td>
<td>Strong industry & services</td>
</tr>
<tr>
<td>Inflation</td>
<td>β€”</td>
<td>3.57% (June)</td>
<td>β€”</td>
<td>May: 3.24%</td>
</tr>
<tr>
<td>Unemployment</td>
<td>2.20%</td>
<td>β€”</td>
<td>β€”</td>
<td>Q1 2025</td>
</tr>
<tr>
<td>FDI (Registered, 5 months)</td>
<td colspan="2">$18.4B (registered)</td>
<td>$21.51B (H1 total)</td>
<td>+32.6% YoY</td>
</tr>
<tr>
<td>Retail Sales</td>
<td colspan="3">1.708 quadrillion VND (Q1) β€” 9.9% YoY</td>
<td>Consumption growth</td>
</tr>
</tbody>
</table>
<div style="display:flex;gap:8px;margin-top:8px">
<button class="btn" id="exportTableCsv"><i class="fa fa-file-csv"></i> Export CSV</button>
<button class="btn ghost" id="sortTable"><i class="fa fa-sort"></i> Sort GDP rows</button>
</div>
</div>
</section>
</section>
</section>
<div style="display:flex;gap:var(--gap);flex-wrap:wrap;align-items:center;justify-content:space-between">
<div class="small muted">Last updated: 2025 β€’ Dashboard generated from report text</div>
<div style="display:flex;gap:8px">
<div class="small muted">Saved view: <span id="savedLabel">none</span></div>
</div>
</div>
</main>
<div class="progress-wrap" aria-hidden>
<div class="progress"><i id="progBar"></i></div>
<div class="tag small muted" id="progPct">0%</div>
</div>
<div class="tooltip" id="tooltip" role="tooltip" aria-hidden="true"></div>
<footer>
<div>Β© Vietnam Economic Dashboard β€” 2025</div>
<div class="small muted">Built with Web APIs β€’ Interactive β€’ Single-file</div>
</footer>
</div>
<script>
/***************
* Data model
***************/
const DATA = {
historyQ1: [
{year:2020, q1:3.21},
{year:2021, q1:4.85},
{year:2022, q1:5.42},
{year:2023, q1:3.46},
{year:2024, q1:5.98},
{year:2025, q1:6.93}
],
q2025: {q1:6.9, q2:7.96, h1:7.52},
forecasts: [
{name:"World Bank", value:5.8, color:"#0ea5a4"},
{name:"ADB", value:6.6, color:"#7c3aed"},
{name:"IMF", value:5.2, color:"#f59e0b"},
{name:"Government", value:8.4, color:"#ef4444"}
],
inflation: [
{label:"May 2025", value:3.24},
{label:"June 2025", value:3.57}
],
unemployment_q1:2.20,
fdi: {
registered_5m:18.4,
disbursed_5m:8.9,
total_h1:21.51,
yoy:32.6
},
retail_q1_vnd:1708, // trillion? using quadrillion as textual; keep numeric for note
sectorShares: [
{name:"Services", value:45, color:"#06b6d4"},
{name:"Manufacturing", value:30, color:"#7c3aed"},
{name:"Exports", value:15, color:"#34d399"},
{name:"Banking", value:10, color:"#f97316"}
]
};
/*******************
* Utilities
*******************/
function el(sel){return document.querySelector(sel)}
function els(sel){return Array.from(document.querySelectorAll(sel))}
// Smooth scroll for TOC links
els('.toc a').forEach(a=>{
a.addEventListener('click', (e)=>{
e.preventDefault();
const id = a.getAttribute('data-target');
const node = el('#'+id);
if(node) node.scrollIntoView({behavior:'smooth', block:'start'});
els('.toc a').forEach(x=>x.classList.remove('active')); a.classList.add('active');
});
});
// Back to top & save view
el('#backTop').addEventListener('click', ()=>window.scrollTo({top:0,behavior:'smooth'}));
el('#saveView').addEventListener('click', ()=>{
const view = {
timestamp: Date.now(),
filters: {highlight:el('#globalSearch').value||null}
};
localStorage.setItem('vn_report_view', JSON.stringify(view));
el('#savedLabel').textContent = new Date(view.timestamp).toLocaleString();
flash('Saved view');
});
// Load saved view if present
(function(){
const v = localStorage.getItem('vn_report_view');
if(v){ try{
const view = JSON.parse(v);
el('#savedLabel').textContent = new Date(view.timestamp).toLocaleString();
}catch(e){}}
})();
// Simple flash notification using tooltip element
function flash(text, ms=1200){
const t = el('#tooltip');
t.textContent = text; t.style.opacity=1; t.style.transform='translate(-50%,-60%)';
t.style.left = '50%'; t.style.top='20%';
setTimeout(()=>{t.style.opacity=0}, ms);
}
/*******************
* Search & highlight
*******************/
el('#globalSearch').addEventListener('input', (e)=>{
const q = e.target.value.trim().toLowerCase();
if(!q){ // clear highlights
els('.content *').forEach(n=>n.style.outline='');
return;
}
// naive search: highlight elements containing text
els('.content p, .content li, .content td, .content h3, .content .small').forEach(node=>{
const txt = node.textContent.toLowerCase();
if(txt.includes(q)){
node.style.outline = '2px solid rgba(14,165,164,0.15)';
} else {
node.style.outline = '';
}
});
});
/*******************
* Copy executive summary & export
*******************/
const execText = el('#execText').textContent.trim();
el('#copyExec').addEventListener('click', async ()=>{
try{
await navigator.clipboard.writeText(execText);
flash('Executive summary copied');
}catch(e){flash('Unable to copy')};
});
el('#copySummary').addEventListener('click', async ()=>{
const fullText = `Vietnam Economic Growth Report 2025 β€” Executive Summary:\n\n${execText}\n\nSources: GSO, IMF, ADB, Trading Economics`;
try{ await navigator.clipboard.writeText(fullText); flash('Summary copied'); }catch(e){ flash('Copy failed') }
});
el('#downloadJson').addEventListener('click', ()=>{
const payload = {
summary: execText,
data: DATA,
generated: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(payload, null, 2)], {type:'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href=url; a.download = 'vn-economic-report-2025.json'; a.click();
URL.revokeObjectURL(url);
flash('JSON downloaded');
});
// Export CSV (indicators)
function csvFromTable(rows){
return rows.map(r => r.map(c => `"${String(c).replace(/"/g,'""')}"`).join(',')).join('\n');
}
el('#exportCsv').addEventListener('click', ()=>{
const rows = [
['Metric','Q1 2025','Q2 2025','H1 2025','Note'],
['GDP Growth (YoY)', DATA.q2025.q1+'%', DATA.q2025.q2+'%', DATA.q2025.h1+'%','Strong industry & services'],
['Inflation','May: 3.24%','June: 3.57%','','IMF:2.9%, ADB:4.0%'],
['Unemployment','2.20%','','','Q1 2025'],
['FDI','Registered (5 months) $18.4B','Disbursed $8.9B','Total H1 $21.51B','+32.6% YoY']
];
const csv = csvFromTable(rows);
const blob = new Blob([csv], {type:'text/csv'});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'vn_indicators_2025.csv';
link.click();
URL.revokeObjectURL(link.href);
flash('CSV exported');
});
// Export table CSV
el('#exportTableCsv').addEventListener('click', ()=>{
const tbody = Array.from(el('#dataTable tbody').rows);
const rows = [['Metric','Q1 2025','Q2 2025','H1 2025','Note']];
tbody.forEach(r=>{
const cells = Array.from(r.cells).map(c=>c.textContent.trim());
rows.push(cells);
});
const csv = csvFromTable(rows);
const blob = new Blob([csv], {type:'text/csv'});
const a = document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='vn_appendix.csv'; a.click();
URL.revokeObjectURL(a.href);
flash('Table CSV exported');
});
// Sort table (basic)
el('#sortTable').addEventListener('click', ()=>{
const tbody = el('#dataTable tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
// sort by numeric in first numeric-like cell (attempt)
rows.sort((a,b)=>{
const ta = a.cells[1].textContent.match(/[\d\.]+/);
const tb = b.cells[1].textContent.match(/[\d\.]+/);
const na = ta? parseFloat(ta[0]) : 0;
const nb = tb? parseFloat(tb[0]) : 0;
return nb - na;
});
rows.forEach(r=>tbody.appendChild(r));
flash('Table sorted');
});
/*******************
* Intersection observer for reveal & progress
*******************/
const progBar = el('#progBar'), progPct = el('#progPct');
function updateProgress(){
const doc = document.documentElement;
const top = doc.scrollTop || document.body.scrollTop;
const h = doc.scrollHeight - doc.clientHeight;
const pct = Math.round((top/h)*100);
progBar.style.width = pct + '%';
progPct.textContent = pct + '%';
}
window.addEventListener('scroll', updateProgress);
window.addEventListener('resize', updateProgress);
updateProgress();
// Reveal animations using IntersectionObserver
const observer = new IntersectionObserver((entries)=>{
entries.forEach(entry=>{
if(entry.isIntersecting){
entry.target.style.transform = 'translateY(0)';
entry.target.style.opacity = 1;
observer.unobserve(entry.target);
}
});
}, {threshold:0.12});
// apply to cards
els('.card').forEach(c=>{
c.style.opacity=0; c.style.transform='translateY(10px)'; c.style.transition='opacity .6s ease, transform .6s ease';
observer.observe(c);
});
/*******************
* Simple SVG charting functions
*******************/
function createSVG(width, height){
const svgNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNS,'svg');
svg.setAttribute('width','100%');
svg.setAttribute('viewBox',`0 0 ${width} ${height}`);
svg.setAttribute('preserveAspectRatio','none');
svg.style.display='block';
return svg;
}
// Line chart for GDP q1 history
function drawGDPLine(elm, data, opts={}){
elm.innerHTML='';
const w = 760, h = 260, pad = 36;
const svg = createSVG(w,h);
const svgNS = "http://www.w3.org/2000/svg";
const values = data.map(d => d.q1);
const minVal = Math.min(...values) - 1;
const maxVal = Math.max(...values) + 1;
const xStep = (w - pad*2) / (data.length - 1);
// grid lines
for(let i=0;i<5;i++){
const y = pad + i*(h - pad*2)/4;
const line = document.createElementNS(svgNS,'line');
line.setAttribute('x1',pad); line.setAttribute('x2',w-pad);
line.setAttribute('y1',y); line.setAttribute('y2',y);
line.setAttribute('stroke','rgba(255,255,255,0.03)');
line.setAttribute('stroke-width','1');
svg.appendChild(line);
}
// path
const pts = data.map((d,i)=>{
const x = pad + i*xStep;
const y = pad + (1 - (d.q1 - minVal)/(maxVal - minVal))*(h - pad*2);
return [x,y];
});
// optionally do smoothing
const pathData = opts.smooth ? catmullRom2bezier(pts) : ('M '+pts.map(p=>p.join(' ')).join(' L '));
const path = document.createElementNS(svgNS,'path');
path.setAttribute('d', pathData);
path.setAttribute('fill','none');
path.setAttribute('stroke','url(#ggrad)');
path.setAttribute('stroke-width','3');
path.setAttribute('stroke-linejoin','round');
path.setAttribute('stroke-linecap','round');
svg.appendChild(path);
// gradient
const defs = document.createElementNS(svgNS,'defs');
defs.innerHTML = `<linearGradient id="ggrad" x1="0" x2="1" y1="0" y2="0">
<stop offset="0%" stop-color="${DATA.forecasts[0].color}" />
<stop offset="100%" stop-color="${DATA.forecasts[1].color}" />
</linearGradient>`;
svg.appendChild(defs);
// points & labels & hover
const gPoints = document.createElementNS(svgNS,'g');
pts.forEach((p,i)=>{
const c = document.createElementNS(svgNS,'circle');
c.setAttribute('cx',p[0]); c.setAttribute('cy',p[1]); c.setAttribute('r',4);
c.setAttribute('fill','#fff'); c.setAttribute('stroke','rgba(0,0,0,0.2)'); c.setAttribute('stroke-width',1);
c.style.cursor='pointer';
c.addEventListener('mouseenter', (ev)=>showTip(`${data[i].year} Q1: ${data[i].q1}%`, ev.clientX, ev.clientY));
c.addEventListener('mouseleave', hideTip);
gPoints.appendChild(c);
// year labels
const t = document.createElementNS(svgNS,'text');
t.setAttribute('x', p[0]); t.setAttribute('y', h - 6);
t.setAttribute('font-size', '10'); t.setAttribute('text-anchor','middle');
t.setAttribute('fill','rgba(255,255,255,0.6)');
t.textContent = data[i].year;
svg.appendChild(t);
});
svg.appendChild(gPoints);
// axis left values
for(let i=0;i<5;i++){
const v = (maxVal - i*(maxVal - minVal)/4).toFixed(1);
const y = pad + i*(h - pad*2)/4;
const t = document.createElementNS(svgNS,'text');
t.setAttribute('x',6); t.setAttribute('y', y+4);
t.setAttribute('font-size','10'); t.setAttribute('fill','rgba(255,255,255,0.6)');
t.textContent = v + '%';
svg.appendChild(t);
}
elm.appendChild(svg);
}
// Simple bar chart for forecasts
function drawForecastBar(elm, data){
elm.innerHTML='';
const w = 360, h = 240, pad=40;
const svg = createSVG(w,h);
const svgNS="http://www.w3.org/2000/svg";
const max = Math.max(...data.map(d=>d.value)) + 1;
const bw = (w - pad*2)/data.length - 8;
data.forEach((d,i)=>{
const x = pad + i*((w - pad*2)/data.length) + 4;
const y = pad + (1 - d.value/max)*(h - pad*2);
const rect = document.createElementNS(svgNS,'rect');
rect.setAttribute('x', x); rect.setAttribute('y', y);
rect.setAttribute('width', bw); rect.setAttribute('height', h - pad - y);
rect.setAttribute('fill', d.color);
rect.style.cursor='pointer';
rect.addEventListener('mouseenter', (ev)=>showTip(`${d.name}: ${d.value}%`, ev.clientX, ev.clientY));
rect.addEventListener('mouseleave', hideTip);
rect.addEventListener('click', ()=>flash(`${d.name} highlighted`));
svg.appendChild(rect);
const t = document.createElementNS(svgNS,'text');
t.setAttribute('x', x + bw/2); t.setAttribute('y', h - 8); t.setAttribute('text-anchor','middle');
t.setAttribute('font-size','11'); t.setAttribute('fill','rgba(255,255,255,0.8)');
t.textContent = d.name;
svg.appendChild(t);
});
elm.appendChild(svg);
}
// Small sparkline for inflation
function drawSparkline(elm, data){
elm.innerHTML='';
const w = 320, h = 120, pad=20;
const svg = createSVG(w,h); const NS="http://www.w3.org/2000/svg";
const values = data.map(d=>d.value);
const max = Math.max(...values)+0.5;
const min = Math.min(...values)-0.5;
const step = (w - pad*2)/(values.length-1);
const pts = values.map((v,i)=>[pad + i*step, pad + (1-(v-min)/(max-min))*(h - pad*2)]);
// path
const dpath = 'M '+pts.map(p=>p.join(' ')).join(' L ');
const path = document.createElementNS(NS,'path'); path.setAttribute('d',dpath); path.setAttribute('fill','none');
path.setAttribute('stroke','url(#g2)'); path.setAttribute('stroke-width','2');
svg.appendChild(path);
const defs = document.createElementNS(NS,'defs');
defs.innerHTML = `<linearGradient id="g2"><stop offset="0%" stop-color="${DATA.forecasts[0].color}"/><stop offset="100%" stop-color="${DATA.forecasts[2].color}"/></linearGradient>`;
svg.appendChild(defs);
pts.forEach((p,i)=>{
const c = document.createElementNS(NS,'circle'); c.setAttribute('cx',p[0]); c.setAttribute('cy',p[1]); c.setAttribute('r',3); c.setAttribute('fill','#fff');
c.addEventListener('mouseenter', (ev)=>showTip(`${data[i].label}: ${data[i].value}%`, ev.clientX, ev.clientY));
c.addEventListener('mouseleave', hideTip);
svg.appendChild(c);
});
elm.appendChild(svg);
}
// Donut chart for FDI breakdown
function drawDonut(elm, reg, disb){
elm.innerHTML='';
const w = 320, h = 140; const NS="http://www.w3.org/2000/svg";
const svg = createSVG(w,h);
const cx = w/2, cy = h/2, r = 48;
const total = Math.max(reg, disb);
const vals = [{name:'Registered',v:reg,color:'#06b6d4'},{name:'Disbursed',v:disb,color:'#7c3aed'}];
let angle = -90;
vals.forEach(v=>{
const slice = (v.v/total)*360;
const [x1,y1] = pol(angle, r, cx, cy);
angle += slice;
const [x2,y2] = pol(angle, r, cx, cy);
const large = slice>180?1:0;
const d = `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} z`;
const p = document.createElementNS(NS,'path'); p.setAttribute('d',d); p.setAttribute('fill',v.color);
p.addEventListener('mouseenter', (ev)=>showTip(`${v.name}: $${v.v}B`, ev.clientX, ev.clientY));
p.addEventListener('mouseleave', hideTip);
svg.appendChild(p);
});
// centre label
const text = document.createElementNS(NS,'text'); text.setAttribute('x',cx); text.setAttribute('y',cy+6);
text.setAttribute('text-anchor','middle'); text.setAttribute('font-size','12'); text.setAttribute('fill','rgba(255,255,255,0.9)');
text.textContent = `${DATA.fdi.total_h1}B (H1)`;
svg.appendChild(text);
elm.appendChild(svg);
}
// Sector bars
function drawSectorBars(elm, data){
elm.innerHTML='';
const w = 680, h = 140, pad=20;
const svg = createSVG(w,h); const NS="http://www.w3.org/2000/svg";
const max = Math.max(...data.map(d=>d.value));
const bw = (w - pad*2)/data.length - 12;
data.forEach((d,i)=>{
const x = pad + i*((w - pad*2)/data.length) + 6;
const barH = (d.value/max)*(h - pad*2);
const y = h - pad - barH;
const rect = document.createElementNS(NS,'rect');
rect.setAttribute('x',x); rect.setAttribute('y',y); rect.setAttribute('height',barH); rect.setAttribute('width',bw);
rect.setAttribute('rx',6); rect.setAttribute('fill',d.color);
rect.addEventListener('mouseenter', (ev)=>showTip(`${d.name}: ${d.value}% share`, ev.clientX, ev.clientY));
rect.addEventListener('mouseleave', hideTip);
rect.addEventListener('click', ()=>flash(`${d.name} pinned`));
svg.appendChild(rect);
const t = document.createElementNS(NS,'text'); t.setAttribute('x', x + bw/2); t.setAttribute('y', h - 6);
t.setAttribute('text-anchor','middle'); t.setAttribute('font-size','11'); t.setAttribute('fill','rgba(255,255,255,0.8)');
t.textContent = d.name;
svg.appendChild(t);
});
elm.appendChild(svg);
}
// Helpers
function pol(angleDeg, r, cx, cy){
const rad = angleDeg * Math.PI/180;
return [cx + r*Math.cos(rad), cy + r*Math.sin(rad)];
}
function showTip(text, x, y){
const t = el('#tooltip');
t.textContent = text;
t.style.opacity = 1;
t.style.left = (x)+'px';
t.style.top = (y - 16)+'px';
t.setAttribute('aria-hidden','false');
}
function hideTip(){ const t = el('#tooltip'); t.style.opacity = 0; t.setAttribute('aria-hidden','true'); }
// Catmull-Rom spline smoothing to bezier
function catmullRom2bezier(pts){
// pts: array of [x,y]
let d = '';
for(let i=0;i<pts.length-1;i++){
const p0 = i==0 ? pts[i] : pts[i-1];
const p1 = pts[i];
const p2 = pts[i+1];
const p3 = i+2<pts.length ? pts[i+2] : p2;
const bp1x = p1[0] + (p2[0] - p0[0]) / 6;
const bp1y = p1[1] + (p2[1] - p0[1]) / 6;
const bp2x = p2[0] - (p3[0] - p1[0]) / 6;
const bp2y = p2[1] - (p3[1] - p1[1]) / 6;
if(i==0) d += 'M '+p1[0]+' '+p1[1];
d += ' C '+bp1x+' '+bp1y+','+bp2x+' '+bp2y+','+p2[0]+' '+p2[1];
}
return d;
}
/*******************
* Initial draw
*******************/
function renderAll(){
drawGDPLine(el('#gdpChart'), DATA.historyQ1.map(d=>({year:d.year, q1:d.q1})), {smooth:false});
drawForecastBar(el('#forecastBar'), DATA.forecasts);
drawSparkline(el('#inflationSpark'), DATA.inflation);
drawDonut(el('#fdiDonut'), DATA.fdi.registered_5m, DATA.fdi.disbursed_5m);
drawSectorBars(el('#sectorBars'), DATA.sectorShares);
// legend for GDP chart
const lg = el('#gdpLegend'); lg.innerHTML='';
DATA.forecasts.slice(0,2).forEach(f=>{
const btn = document.createElement('button');
btn.textContent = f.name; btn.style.borderLeft = '4px solid ' + f.color;
btn.addEventListener('click', ()=>flash(f.name + ' selected'));
lg.appendChild(btn);
});
}
renderAll();
// Toggle smoothing
let smooth = false;
el('#toggleSmooth').addEventListener('click', ()=>{
smooth = !smooth;
drawGDPLine(el('#gdpChart'), DATA.historyQ1.map(d=>({year:d.year, q1:d.q1})), {smooth});
flash(smooth ? 'Smoothing enabled' : 'Smoothing disabled');
});
// Copy chart SVG (gdp chart)
el('#copyChartSvg').addEventListener('click', async ()=>{
const svgEl = el('#gdpChart svg');
if(!svgEl){ flash('No chart'); return; }
const s = new XMLSerializer().serializeToString(svgEl);
try{
await navigator.clipboard.writeText(s);
flash('Chart SVG copied to clipboard');
}catch(e){ flash('Copy failed') }
});
/*******************
* Interactive drill
*******************/
el('#drillSector').addEventListener('click', ()=>{
// quick filter: highlight services card
flash('Services prioritized: monitor urban services & tourism');
});
// scenario button: quick scenario simulation using simple scaling
el('#scenarioBtn').addEventListener('click', ()=>{
// simulate a downside: global shock reduces exports by 3pp
const sim = {...DATA.q2025};
const drop = 0.9;
const newH1 = +(sim.h1 * drop).toFixed(2);
el('#gdpNow').textContent = `Simulated H1: ${newH1}% (stress)`;
flash('Scenario applied: -10% to H1 growth (exports shock)');
setTimeout(()=>{ el('#gdpNow').textContent = `GDP H1 2025: ${DATA.q2025.h1}%`; flash('Scenario cleared') }, 3500);
});
// Mitigation strategies button
el('#mitigateBtn').addEventListener('click', ()=>{
alert('Recommended mitigation:\n- Diversify export markets\n- Strengthen domestic demand\n- Preserve macro stability\n- Use fiscal buffers to cushion shocks\n- Leverage FDI into high-value sectors');
});
/*******************
* Clipboard copy summary via header theme toggle as extra functionality
*******************/
el('#toggleTheme').addEventListener('click', async ()=>{
// toggles accent color and also copies a mini summary to clipboard
document.documentElement.style.setProperty('--accent', getRandomColor());
try{ await navigator.clipboard.writeText('Vietnam β€” strong H1 2025: GDP H1 7.52%, Q2 7.96%'); flash('Mini-summary copied'); }catch(e){flash('Accent changed') }
});
function getRandomColor(){
const colors = ['#06b6d4','#f59e0b','#ef4444','#7c3aed','#0ea5a4'];
return colors[Math.floor(Math.random()*colors.length)];
}
/*******************
* Accessibility & keyboard shortcuts
*******************/
window.addEventListener('keydown', (e)=>{
if(e.ctrlKey && e.key === 'k'){
e.preventDefault();
el('#globalSearch').focus();
}
if(e.ctrlKey && e.key === 'b'){
e.preventDefault(); el('#backTop').click();
}
});
/*******************
* Final touches: adaptive text content from data
*******************/
// populate dynamic KPI text
el('#gdpNow').textContent = `GDP H1 2025: ${DATA.q2025.h1}%`;
el('#inflNow').textContent = `Inflation June 2025: ${DATA.inflation[1].value}%`;
el('#unempNow').textContent = `Unemployment Q1 2025: ${DATA.unemployment_q1.toFixed(2)}%`;
el('#fdiNow').textContent = `FDI H1 2025: US$${DATA.fdi.total_h1}B (+${DATA.fdi.yoy}% YoY)`;
// Simple cross-reference linking: clicking KPI scrolls to charts
els('.kpi').forEach(k=>{
k.style.cursor='pointer';
k.addEventListener('click', ()=>{
const which = k.getAttribute('data-kpi');
if(which === 'gdp') el('#charts').scrollIntoView({behavior:'smooth'});
if(which === 'inflation') el('#charts').scrollIntoView({behavior:'smooth'});
if(which === 'fdi') el('#sector').scrollIntoView({behavior:'smooth'});
});
});
// On load, small animation
setTimeout(()=>flash('Dashboard ready'), 600);
</script>
</body>
</html>