gpt5-mini-v6 / index.html
thucdangvan020999's picture
Upload index.html with huggingface_hub
afad3ba 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 Presentation</title>
<!-- Icon Library -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
<style>
:root{
--bg: #0f1724;
--card: #0b1220;
--muted: #94a3b8;
--accent: #06b6d4;
--accent-2: #7c3aed;
--success: #10b981;
--danger: #ef4444;
--glass: rgba(255,255,255,0.03);
--radius: 12px;
--max-width: 1200px;
--gap: clamp(12px,2vw,24px);
--ff-sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
color-scheme: dark;
}
*{box-sizing:border-box}
html,body{
height:100%;
margin:0;
font-family:var(--ff-sans);
background:linear-gradient(180deg,var(--bg),#071021 60%);
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
color:#e6eef6;
scroll-behavior:smooth;
font-size:clamp(14px,1.6vw,18px);
line-height:1.45;
padding:24px;
}
/* Layout */
.container{
max-width:var(--max-width);
margin:0 auto;
display:grid;
grid-template-columns: 280px 1fr;
gap:var(--gap);
align-items:start;
}
@media (max-width:1024px){ .container{ grid-template-columns: 1fr; } }
/* TOC */
.toc{
position:sticky;
top:24px;
height: calc(100vh - 48px);
background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);
border-radius:var(--radius);
padding:18px;
display:flex;
flex-direction:column;
gap:16px;
min-height:220px;
box-shadow: 0 6px 18px rgba(2,6,23,0.6);
border:1px solid rgba(255,255,255,0.03);
}
.brand{
display:flex;
gap:12px;
align-items:center;
}
.logo{
width:44px;
height:44px;
border-radius:8px;
display:grid;
place-items:center;
background:linear-gradient(135deg,var(--accent),var(--accent-2));
font-weight:700;
font-size:18px;
color:white;
box-shadow:0 6px 20px rgba(7,11,27,0.6);
}
h1{font-size:clamp(16px,2vw,20px);margin:0}
.toc-content{overflow:auto;padding-right:6px}
.toc ul{list-style:none;padding:0;margin:6px 0 0 0;display:flex;flex-direction:column;gap:8px}
.toc a{
color:var(--muted);
text-decoration:none;
font-size:14px;
display:flex;
gap:8px;
align-items:center;
padding:8px;
border-radius:8px;
}
.toc a.active, .toc a:hover{ background:var(--glass); color:#fff;}
.toc .small{font-size:12px;color:var(--muted)}
.search-toc{display:flex;gap:8px}
.search-toc input{
background:transparent;border:1px solid rgba(255,255,255,0.04);padding:8px;border-radius:8px;color:var(--muted);width:100%;
}
.controls{display:flex;gap:8px;align-items:center;margin-top:auto}
.btn{
background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted);padding:8px 10px;border-radius:8px;cursor:pointer;font-size:13px;
}
.btn.primary{background:linear-gradient(90deg,var(--accent),var(--accent-2));border:none;color:white;box-shadow:0 8px 24px rgba(7,11,27,0.6)}
.progress-mini{height:6px;background:rgba(255,255,255,0.03);border-radius:6px;overflow:hidden}
.progress-mini > i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--accent-2));width:0%}
/* Main area */
main{
display:grid;
gap:var(--gap);
}
.card{
background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
border-radius:var(--radius);
padding:18px;
box-shadow:0 8px 30px rgba(2,6,23,0.6);
border:1px solid rgba(255,255,255,0.03);
}
header.card{
display:flex;
gap:18px;
align-items:flex-start;
position:relative;
overflow:visible;
}
.summary{
flex:1;
display:flex;
flex-direction:column;
gap:8px;
}
.kpis{display:flex;gap:12px;flex-wrap:wrap}
.kpi{
background:rgba(255,255,255,0.02);
padding:12px;border-radius:10px;min-width:120px;flex:1;
display:flex;flex-direction:column;gap:6px;
}
.kpi .value{font-size:clamp(18px,2.4vw,24px);font-weight:700}
.kpi .label{font-size:12px;color:var(--muted)}
.actions{display:flex;gap:8px;align-items:center}
.chip{padding:6px 10px;border-radius:999px;background:rgba(255,255,255,0.02);font-size:13px;color:var(--muted);display:inline-flex;gap:8px;align-items:center}
/* Content sections */
section{display:grid;gap:12px}
.section-title{display:flex;justify-content:space-between;align-items:center;gap:12px}
.section-title h2{margin:0;font-size:18px}
.subtle{color:var(--muted);font-size:13px}
/* Grid for visualizations */
.viz-grid{
display:grid;
gap:12px;
grid-template-columns:repeat(2,1fr);
}
@media (max-width:1024px){ .viz-grid{ grid-template-columns:1fr; } }
.chart{
background:linear-gradient(180deg, rgba(255,255,255,0.015), transparent);
padding:12px;border-radius:10px;min-height:220px;display:flex;flex-direction:column;gap:8px;
border:1px solid rgba(255,255,255,0.02);
}
.chart .title{display:flex;justify-content:space-between;align-items:center}
.svg-wrap{flex:1;display:grid;place-items:center;padding:6px}
svg{width:100%;height:100%}
/* Table */
.data-table{overflow:auto;border-radius:10px;border:1px solid rgba(255,255,255,0.03)}
table{width:100%;border-collapse:collapse;color:var(--muted);min-width:640px}
th,td{padding:12px 10px;text-align:left;border-bottom:1px dashed rgba(255,255,255,0.02)}
th{color:#cfe8f1;cursor:pointer;position:relative}
th .sort{opacity:0.6;margin-left:8px}
tr:hover td{background:linear-gradient(90deg, rgba(255,255,255,0.01), transparent);color:#fff}
/* Callouts & insights */
.callouts{display:flex;gap:12px;flex-wrap:wrap}
.callout{flex:1;min-width:220px;padding:12px;border-radius:10px;background:linear-gradient(180deg, rgba(124,58,237,0.06), rgba(6,182,212,0.03));border:1px solid rgba(255,255,255,0.02)}
.callout h3{margin:0 0 6px 0;font-size:14px}
.callout p{margin:0;color:var(--muted);font-size:13px}
/* Collapsible */
.collapsible{border-radius:8px;overflow:hidden}
.collapsible summary{list-style:none;cursor:pointer;padding:12px;background:rgba(255,255,255,0.02);display:flex;justify-content:space-between;align-items:center}
.collapsible details{background:transparent}
/* Footer */
footer{display:flex;justify-content:space-between;align-items:center;color:var(--muted);font-size:13px;padding-top:6px}
footer .sources{display:flex;gap:8px;flex-wrap:wrap}
/* Annotations and badges */
.badge{padding:6px 8px;border-radius:8px;background:rgba(255,255,255,0.02);font-size:12px}
.legend{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.legend span{display:flex;gap:8px;align-items:center;padding:6px;border-radius:8px;background:rgba(255,255,255,0.02)}
.legend i{width:14px;height:14px;border-radius:3px;display:inline-block}
/* Responsive typography with clamp */
h2{font-size:clamp(16px,2.2vw,20px)}
p{margin:0}
/* heatmap cells */
.heatmap-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:6px}
.heat-cell{aspect-ratio:1/1;border-radius:6px;display:grid;place-items:center;font-size:12px;color:#02111a;padding:6px;font-weight:700;}
/* small helpers */
.muted{color:var(--muted)}
.right{text-align:right}
.hidden{display:none}
/* Container query example */
.card:container(min-width:600px){
padding:20px;
}
</style>
</head>
<body>
<div class="container">
<!-- TOC -->
<aside class="toc card" aria-label="Table of contents">
<div class="brand">
<div class="logo">VN</div>
<div>
<h1>Vietnam Economic Growth 2025</h1>
<div class="small muted">Interactive report & visualization</div>
</div>
</div>
<div class="search-toc">
<input id="tocFilter" placeholder="Search sections..." aria-label="Search sections" />
<button class="btn" id="themeToggle" title="Toggle theme"><i class="fa fa-sun"></i></button>
</div>
<nav class="toc-content" id="tocList">
<ul>
<li><a href="#executive" data-target="executive"><i class="fa fa-file-lines"></i> Executive Summary</a></li>
<li><a href="#indicators" data-target="indicators"><i class="fa fa-chart-line"></i> Key Indicators</a></li>
<li><a href="#sectoral" data-target="sectoral"><i class="fa fa-industry"></i> Sectoral Analysis</a></li>
<li><a href="#visualizations" data-target="visualizations"><i class="fa fa-chart-pie"></i> Visualizations</a></li>
<li><a href="#table" data-target="table"><i class="fa fa-table"></i> Data Table</a></li>
<li><a href="#outlook" data-target="outlook"><i class="fa fa-eye"></i> Outlook & Risks</a></li>
<li><a href="#appendix" data-target="appendix"><i class="fa fa-book"></i> References & Appendix</a></li>
</ul>
</nav>
<div class="controls">
<button class="btn" id="copySummary" title="Copy executive summary"><i class="fa fa-copy"></i></button>
<button class="btn primary" id="exportCSV" title="Export table CSV"><i class="fa fa-download"></i> Export</button>
</div>
<div style="margin-top:10px">
<div class="small muted">Progress</div>
<div class="progress-mini"><i id="overallProgress" style="width:12%"></i></div>
</div>
</aside>
<!-- Main -->
<main>
<!-- Header / Executive summary -->
<header id="executive" class="card">
<div class="summary">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px">
<div>
<h2>Executive Summary</h2>
<div class="subtle">Vietnam's economy shows robust mid-year growth led by services and manufacturing.</div>
</div>
<div class="badge">Report 2025 • Updated</div>
</div>
<p class="muted" style="margin-top:8px">
Vietnam recorded 7.52% GDP growth in H1 2025 — the highest mid-year performance since 2011. Q2 2025 grew 7.96% YoY. Strong FDI, low unemployment and controlled inflation underpin growth despite global trade tensions.
</p>
<div class="kpis" style="margin-top:12px">
<div class="kpi">
<div class="value" id="kpiGdp">7.52%</div>
<div class="label">H1 2025 GDP Growth (YoY)</div>
</div>
<div class="kpi">
<div class="value" id="kpiQ2">7.96%</div>
<div class="label">Q2 2025 GDP Growth (YoY)</div>
</div>
<div class="kpi">
<div class="value" id="kpiInfl">3.57%</div>
<div class="label">June 2025 Inflation</div>
</div>
<div class="kpi">
<div class="value" id="kpiUnemp">2.20%</div>
<div class="label">Unemployment Q1 2025</div>
</div>
</div>
<div style="display:flex;gap:12px;margin-top:12px;align-items:center">
<div class="chip"><i class="fa fa-building"></i> FDI Inflows +32.6% YoY</div>
<div class="chip muted">Government target: 8.3–8.5%</div>
<div class="chip muted">World Bank: 5.8% • IMF: 5.2%</div>
</div>
</div>
<div style="width:320px;display:flex;flex-direction:column;gap:12px">
<div class="card" style="background:linear-gradient(180deg,#061626,#082038);padding:12px">
<div style="display:flex;justify-content:space-between;align-items:center">
<div class="muted">Forecast vs Actual</div>
<div class="badge">Citations</div>
</div>
<div style="margin-top:12px;display:grid;gap:8px">
<div style="display:flex;justify-content:space-between">
<div class="small muted">Govt Target</div><strong>8.3–8.5%</strong>
</div>
<div style="display:flex;justify-content:space-between">
<div class="small muted">ADB</div><strong>6.6%</strong>
</div>
<div style="display:flex;justify-content:space-between">
<div class="small muted">World Bank</div><strong>5.8%</strong>
</div>
<div style="display:flex;justify-content:space-between">
<div class="small muted">IMF</div><strong>5.2%</strong>
</div>
</div>
<div style="margin-top:10px" class="subtle">Sources: IMF, ADB, World Bank, Government statistics (see References)</div>
</div>
<div class="card" style="padding:10px">
<div style="display:flex;justify-content:space-between;align-items:center">
<div><strong>Actionable Insight</strong><div class="muted" style="font-size:13px">Policy & Business Implication</div></div>
<i class="fa fa-lightbulb" style="color:var(--accent);font-size:20px"></i>
</div>
<p class="muted" style="margin-top:8px">Sustain growth while prioritizing macroeconomic stability: diversify export markets, manage credit growth and use targeted fiscal support to handle external shocks.</p>
</div>
</div>
</header>
<!-- Key Indicators -->
<section id="indicators" class="card">
<div class="section-title">
<h2>Key Economic Indicators</h2>
<div class="muted">Press a chart's legend to toggle series. Hover for details.</div>
</div>
<div class="viz-grid">
<div class="chart" id="chartGDPQuarterly" data-src="gdp-quarter">
<div class="title">
<div><strong>Quarterly GDP Growth (2024–2025)</strong><div class="muted">YoY %</div></div>
<div class="legend" id="legendGDPQ"></div>
</div>
<div class="svg-wrap"><svg id="svgGDPQ" viewBox="0 0 800 300" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Quarterly GDP Growth chart"></svg></div>
<div class="subtle">Source: General Statistics Office, IMF</div>
</div>
<div class="chart" id="chartGDPAnnual" data-src="gdp-annual">
<div class="title">
<div><strong>Annual GDP Growth (2020–2025)</strong><div class="muted">Yearly %</div></div>
<div class="legend" id="legendGPDA"></div>
</div>
<div class="svg-wrap"><svg id="svgGPDA" viewBox="0 0 800 300" preserveAspectRatio="xMidYMid meet"></svg></div>
<div class="subtle">Includes forecast & historical comparison</div>
</div>
<div class="chart" id="chartInflation">
<div class="title">
<div><strong>Inflation (May–June 2025) & Forecasts</strong><div class="muted">%, month & forecast</div></div>
<div class="legend" id="legendInfl"></div>
</div>
<div class="svg-wrap"><svg id="svgInfl" viewBox="0 0 800 220" preserveAspectRatio="xMidYMid meet"></svg></div>
<div class="subtle">IMF: 2.9% • ADB: 4.0%</div>
</div>
<div class="chart" id="chartSector">
<div class="title">
<div><strong>Sector Contribution (Est.)</strong><div class="muted">Share of GDP</div></div>
<div class="legend" id="legendSector"></div>
</div>
<div class="svg-wrap"><svg id="svgSector" viewBox="0 0 800 300" preserveAspectRatio="xMidYMid meet"></svg></div>
<div class="subtle">Services & Manufacturing drive growth</div>
</div>
</div>
</section>
<!-- Sectoral Analysis -->
<section id="sectoral" class="card">
<div class="section-title">
<h2>Sectoral Analysis</h2>
<div class="muted">Performance breakdown & retail trends</div>
</div>
<div style="display:grid;grid-template-columns:1fr 320px;gap:12px">
<div>
<div class="callouts">
<div class="callout">
<h3>Services</h3>
<p>Primary contributor to H1 2025 growth — strong domestic consumption and tourism recovery.</p>
</div>
<div class="callout">
<h3>Manufacturing</h3>
<p>Export-oriented manufacturing supported by FDI inflows and supply chain relocation.</p>
</div>
<div class="callout">
<h3>Banking</h3>
<p>Projected earnings +17% in 2025 on credit growth ~15%.</p>
</div>
</div>
<div style="margin-top:12px">
<details class="collapsible" open>
<summary><strong>Retail Performance & Numbers</strong><span class="muted"> (Q1 2025)</span></summary>
<div style="padding:12px">
<p class="muted">Retail sales reached 1.708 quadrillion VND (US$66.83B), +9.9% YoY, indicating resilient domestic demand.</p>
<div style="margin-top:8px">
<button class="btn" id="copyRetail">Copy retail data</button>
<button class="btn" id="openScatter">View FDI vs GDP scatter</button>
</div>
</div>
</details>
</div>
</div>
<aside style="display:flex;flex-direction:column;gap:12px">
<div class="card" style="padding:12px;background:linear-gradient(180deg, rgba(255,255,255,0.01), transparent)">
<div style="display:flex;justify-content:space-between;align-items:center">
<div><strong>FDI Snapshot</strong><div class="muted" style="font-size:13px">First half 2025</div></div>
<div class="badge">+32.6% YoY</div>
</div>
<div style="margin-top:12px">
<div style="display:flex;justify-content:space-between"><div class="muted">Registered capital (5 months)</div><strong>$18.4B</strong></div>
<div style="display:flex;justify-content:space-between"><div class="muted">Disbursed capital</div><strong>$8.9B</strong></div>
<div style="display:flex;justify-content:space-between"><div class="muted">Total FDI (H1)</div><strong>$21.51B</strong></div>
</div>
</div>
<div class="card" style="padding:12px">
<strong>Risks</strong>
<ul class="muted" style="margin:8px 0 0 18px;line-height:1.6">
<li>Trade tensions & tariffs</li>
<li>Overdependence on FDI</li>
<li>Geopolitical uncertainty</li>
<li>Macro stability vs fast growth</li>
</ul>
</div>
</aside>
</div>
</section>
<!-- Visualizations & interactive -->
<section id="visualizations" class="card">
<div class="section-title">
<h2>Interactive Visualizations</h2>
<div class="muted">Engage with data — export charts, toggle series, and filter datasets.</div>
</div>
<div style="display:grid;gap:12px">
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center">
<input id="filterSeries" placeholder="Filter by sector (e.g., Services)" style="padding:8px;border-radius:8px;background:transparent;border:1px solid rgba(255,255,255,0.03);color:var(--muted);width:240px"/>
<button class="btn" id="exportPNG"><i class="fa fa-image"></i> Export Selected Chart PNG</button>
<button class="btn" id="resetView">Reset filters</button>
<div class="muted" style="margin-left:auto">Tip: Click legends to toggle series</div>
</div>
<div class="viz-grid">
<div class="chart" id="chartScatter">
<div class="title">
<div><strong>FDI vs GDP Growth (H1 2025)</strong><div class="muted">Scatter plot</div></div>
<div class="legend" id="legendScatter"></div>
</div>
<div class="svg-wrap"><svg id="svgScatter" viewBox="0 0 800 300" preserveAspectRatio="xMidYMid meet"></svg></div>
<div class="subtle">Shows correlation between regional FDI & quarter growth</div>
</div>
<div class="chart" id="chartHeatmap">
<div class="title">
<div><strong>Monthly Indicator Heatmap (Jan–Jun 2025)</strong><div class="muted">Inflation, FDI & Retail</div></div>
</div>
<div class="svg-wrap" style="padding:10px">
<div id="heatmap" class="heatmap-grid" style="width:100%"></div>
</div>
<div class="subtle">Color intensity reflects magnitude; hover for exact values</div>
</div>
</div>
</div>
</section>
<!-- Data table -->
<section id="table" class="card">
<div class="section-title">
<h2>Searchable & Sortable Data Table</h2>
<div style="display:flex;gap:8px;align-items:center">
<input id="tableSearch" placeholder="Search table..." style="padding:8px;border-radius:8px;background:transparent;border:1px solid rgba(255,255,255,0.03);color:var(--muted)"/>
<button class="btn" id="copyTable">Copy selection</button>
</div>
</div>
<div class="data-table" style="margin-top:8px">
<table id="dataTable" aria-label="Economic indicators table">
<thead>
<tr>
<th data-key="period">Period <span class="sort muted"></span></th>
<th data-key="gdp">GDP Growth %</th>
<th data-key="inflation">Inflation %</th>
<th data-key="unemployment">Unemployment %</th>
<th data-key="fdi">FDI (US$B)</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
</section>
<!-- Outlook and references -->
<section id="outlook" class="card">
<div class="section-title">
<h2>Outlook & Policy Recommendations</h2>
<div class="muted">Near-term prospects, risks and mitigation</div>
</div>
<div style="display:grid;grid-template-columns:1fr 320px;gap:12px">
<div>
<h3 style="margin-top:0">Near-term Prospects</h3>
<p class="muted">Vietnam began 2025 strongly. However, external trade tensions and uncertainty may moderate growth. Strong domestic fundamentals (FDI, low unemployment, controlled inflation) support a resilient outcome.</p>
<h3 style="margin-top:12px">Risk Mitigation</h3>
<ul class="muted" style="margin:6px 0 0 18px;line-height:1.6">
<li>Diversify export markets and move up value chains</li>
<li>Manage credit growth to avoid overheating and inflation</li>
<li>Strengthen social safety nets and fiscal space for targeted support</li>
<li>Encourage sustainable FDI and local value addition</li>
</ul>
</div>
<aside>
<div class="card" style="padding:12px">
<div style="display:flex;justify-content:space-between;align-items:center">
<div><strong>Projection Scenarios</strong><div class="muted" style="font-size:13px">Base / Optimistic / Downside</div></div>
<div class="badge">2025</div>
</div>
<div style="margin-top:10px">
<div style="display:flex;justify-content:space-between"><div class="muted">Base</div><strong>5.8–6.6%</strong></div>
<div style="display:flex;justify-content:space-between"><div class="muted">Optimistic</div><strong>7.0–8.5%</strong></div>
<div style="display:flex;justify-content:space-between"><div class="muted">Downside</div><strong>3.5–5.0%</strong></div>
</div>
</div>
<div class="card" style="padding:10px;margin-top:12px">
<strong>Quick Links</strong>
<div class="muted" style="margin-top:8px;font-size:13px">IMF • ADB • World Bank • GSO</div>
</div>
</aside>
</div>
</section>
<!-- Appendix / References -->
<section id="appendix" class="card">
<div class="section-title">
<h2>References & Appendix</h2>
<div class="muted">Source attribution and raw data</div>
</div>
<div style="display:flex;gap:12px;align-items:flex-start;flex-wrap:wrap">
<div style="flex:1">
<h3 style="margin-top:0">Sources</h3>
<ol class="muted" id="references">
<li>Trading Economics - Vietnam GDP Annual Growth Rate (tradingeconomics.com)</li>
<li>International Monetary Fund - Vietnam Country Profile (imf.org)</li>
<li>World Bank - Vietnam (worldbank.org)</li>
<li>General Statistics Office - Vietnam (gso.gov.vn)</li>
<li>FocusEconomics, Vietnam Briefing, Vietnam Investment Review</li>
</ol>
</div>
<div style="width:320px">
<details open>
<summary><strong>Appendix: Raw Data</strong></summary>
<pre id="rawData" style="white-space:pre-wrap;background:transparent;color:var(--muted);margin-top:8px;font-size:13px"></pre>
</details>
</div>
</div>
<footer style="margin-top:12px">
<div class="muted">© Vietnam Economic Growth Report 2025 — Interactive</div>
<div class="sources"><span class="muted">Data sources available in references</span></div>
</footer>
</section>
</main>
</div>
<script>
/* ========= Data Model ========= */
const DATA = {
// Quarterly GDP growth (YoY) for 2024 Q1-Q4 and 2025 Q1-Q2
gdpQuarterly: [
{period:'2024 Q1', value:5.98}, {period:'2024 Q2', value:6.3}, {period:'2024 Q3', value:6.8}, {period:'2024 Q4', value:7.1},
{period:'2025 Q1', value:6.9}, {period:'2025 Q2', value:7.96}
],
// Annual GDP 2020-2025 (last is 2025 forecast/highlight)
gdpAnnual: [
{year:2020, value:3.21}, {year:2021, value:4.85}, {year:2022, value:5.42}, {year:2023, value:3.46}, {year:2024, value:5.98}, {year:2025, value:7.52}
],
inflation: [
{period:'May 2025', value:3.24}, {period:'June 2025', value:3.57}, {period:'IMF Forecast 2025', value:2.9}, {period:'ADB Forecast 2025', value:4.0}
],
sectors: [
{name:'Services', share:45, color:'#06b6d4'},
{name:'Manufacturing', share:28, color:'#7c3aed'},
{name:'Agriculture', share:10, color:'#34d399'},
{name:'Construction', share:8, color:'#f59e0b'},
{name:'Other', share:9, color:'#ef4444'}
],
monthly: {
months: ['Jan','Feb','Mar','Apr','May','Jun'],
inflation: [2.8, 2.9, 3.1, 3.0, 3.24, 3.57],
fdiMonthlyB: [2.8, 3.1, 3.4, 4.0, 3.5, 4.7], // illustrative
retailIndex: [98,99,100,102,104,106]
},
regionsFDI: [
{region:'North', fdi:7.0, gdpGrowth:7.2, pts:1},
{region:'Central', fdi:3.5, gdpGrowth:6.1, pts:2},
{region:'South', fdi:11.0, gdpGrowth:8.0, pts:3}
],
tableRows: [
{period:'2020', gdp:3.21, inflation:3.0, unemployment:3.2, fdi:9.1},
{period:'2021', gdp:4.85, inflation:2.6, unemployment:2.9, fdi:10.5},
{period:'2022', gdp:5.42, inflation:3.5, unemployment:2.7, fdi:12.0},
{period:'2023', gdp:3.46, inflation:2.9, unemployment:2.6, fdi:14.8},
{period:'2024', gdp:5.98, inflation:3.1, unemployment:2.22, fdi:16.3},
{period:'2025 H1', gdp:7.52, inflation:3.57, unemployment:2.20, fdi:21.51}
],
references: [
{title:'Trading Economics - Vietnam GDP Annual Growth Rate', url:'https://tradingeconomics.com/vietnam/gdp-growth-annual'},
{title:'IMF - Vietnam Country Profile', url:'https://www.imf.org/en/Countries/VNM'},
{title:'World Bank - Vietnam', url:'https://www.worldbank.org/en/country/vietnam'},
{title:'General Statistics Office - Vietnam', url:'https://www.gso.gov.vn/en/'}
]
};
/* ========= Utilities ========= */
function q(selector, ctx=document){ return ctx.querySelector(selector) }
function qq(selector, ctx=document){ return Array.from(ctx.querySelectorAll(selector)) }
function format(n, decimals=2){ return (Math.round(n*Math.pow(10,decimals))/Math.pow(10,decimals)).toLocaleString(); }
/* ========= Populate Raw Data & References ========= */
document.getElementById('rawData').textContent = JSON.stringify(DATA, null, 2);
const refsEl = q('#references');
refsEl.innerHTML = '';
DATA.references.forEach(r=>{
const li = document.createElement('li');
li.className = 'muted';
li.innerHTML = `<a href="${r.url}" target="_blank" rel="noreferrer" style="color:var(--muted)">${r.title}</a>`;
refsEl.appendChild(li);
});
/* ========= Table Rendering, Search & Sorting ========= */
const tableBody = q('#tableBody');
let tableRows = [...DATA.tableRows];
let currentSort = {key:null, dir:1};
function renderTable(rows){
tableBody.innerHTML = '';
rows.forEach(r=>{
const tr = document.createElement('tr');
tr.innerHTML = `<td>${r.period}</td><td>${r.gdp}</td><td>${r.inflation}</td><td>${r.unemployment}</td><td>${r.fdi}</td>`;
tableBody.appendChild(tr);
});
}
renderTable(tableRows);
q('#tableSearch').addEventListener('input', e=>{
const qv = e.target.value.toLowerCase();
const filtered = tableRows.filter(r => JSON.stringify(r).toLowerCase().includes(qv));
renderTable(filtered);
});
qq('th[data-key]').forEach(th=>{
th.addEventListener('click', ()=>{
const key = th.getAttribute('data-key');
const dir = currentSort.key === key ? -currentSort.dir : 1;
currentSort = {key, dir};
tableRows.sort((a,b)=>{
if (a[key] < b[key]) return -1*dir;
if (a[key] > b[key]) return 1*dir;
return 0;
});
renderTable(tableRows);
});
});
/* ========= Copy & Export Handlers ========= */
q('#copySummary').addEventListener('click', async ()=>{
const text = `Executive Summary:\nVietnam recorded 7.52% GDP growth in H1 2025. Q2 2025 grew 7.96% YoY. Strong FDI, low unemployment (2.20%), and controlled inflation (3.57% June) underpin growth. Sources: GSO, IMF, ADB, World Bank.`;
await navigator.clipboard.writeText(text);
flashButton(q('#copySummary'), 'Copied');
});
q('#copyRetail').addEventListener('click', async ()=>{
const txt = 'Q1 2025 Retail Sales: 1.708 quadrillion VND (~US$66.83B), +9.9% YoY.';
await navigator.clipboard.writeText(txt);
flashButton(q('#copyRetail'),'Copied');
});
q('#copyTable').addEventListener('click', async ()=>{
// copy current table view
let rows = Array.from(tableBody.querySelectorAll('tr')).map(tr => Array.from(tr.children).map(td=>td.textContent).join('\t')).join('\n');
await navigator.clipboard.writeText(rows);
flashButton(q('#copyTable'),'Copied');
});
q('#exportCSV').addEventListener('click', ()=>{
const rows = tableRows;
const csv = ['Period,GDP Growth %,Inflation %,Unemployment %,FDI (US$B)', ...rows.map(r=>`${r.period},${r.gdp},${r.inflation},${r.unemployment},${r.fdi}`)].join('\n');
const blob = new Blob([csv], {type:'text/csv'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href=url; a.download='vietnam_econ_2025.csv'; a.click(); URL.revokeObjectURL(url);
});
function flashButton(btn, text){
const prev = btn.innerHTML;
btn.innerHTML = `<i class="fa fa-check"></i> ${text}`;
setTimeout(()=>btn.innerHTML = prev, 1400);
}
/* ========= Theme toggle using localStorage ========= */
const themeToggle = q('#themeToggle');
function applyTheme(dark=true){
if(!dark){
document.documentElement.style.setProperty('--bg','#f7fbff');
document.documentElement.style.setProperty('--card','#ffffff');
document.documentElement.style.setProperty('--muted','#475569');
document.documentElement.style.setProperty('--accent','#0ea5a4');
document.documentElement.style.setProperty('--accent-2','#8b5cf6');
document.documentElement.style.setProperty('--glass','rgba(0,0,0,0.04)');
document.body.style.color = '#071a2a';
} else {
document.documentElement.style.setProperty('--bg','#0f1724');
document.documentElement.style.setProperty('--card','#0b1220');
document.documentElement.style.setProperty('--muted','#94a3b8');
document.documentElement.style.setProperty('--accent','#06b6d4');
document.documentElement.style.setProperty('--accent-2','#7c3aed');
document.documentElement.style.setProperty('--glass','rgba(255,255,255,0.03)');
document.body.style.color = '#e6eef6';
}
localStorage.setItem('themeDark', dark ? '1' : '0');
themeToggle.innerHTML = `<i class="fa ${dark ? 'fa-moon' : 'fa-sun'}"></i>`;
}
themeToggle.addEventListener('click', ()=> applyTheme(localStorage.getItem('themeDark')==='1' ? false : true));
applyTheme(localStorage.getItem('themeDark') !== '0');
/* ========= SVG Chart Helpers ========= */
function createSVG(w=800,h=300){
const svgns = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgns,'svg');
svg.setAttribute('viewBox',`0 0 ${w} ${h}`);
return svg;
}
// utility to map value onto axis
function linearScale(domainMin, domainMax, outMin, outMax){
const m = (outMax - outMin) / (domainMax - domainMin || 1);
return function(v){ return outMin + (v - domainMin) * m; };
}
// tooltip helper
let tooltip = document.createElement('div');
tooltip.style.position='fixed';
tooltip.style.pointerEvents='none';
tooltip.style.background='rgba(2,6,23,0.9)';
tooltip.style.padding='8px 10px';
tooltip.style.borderRadius='8px';
tooltip.style.color='#dff6ff';
tooltip.style.fontSize='13px';
tooltip.style.display='none';
tooltip.style.zIndex=9999;
document.body.appendChild(tooltip);
function showTip(html, x, y){
tooltip.innerHTML = html;
tooltip.style.left = (x+12)+'px';
tooltip.style.top = (y+12)+'px';
tooltip.style.display = 'block';
tooltip.style.opacity = '1';
}
function hideTip(){ tooltip.style.display='none'; }
/* ========= Draw Quarterly GDP (Line + Bars) ========= */
function drawGDPQuarter(){
const svg = q('#svgGDPQ');
svg.innerHTML = '';
const w = 800, h = 300, pad = {l:48,r:20,t:20,b:40};
const data = DATA.gdpQuarterly;
const xs = data.map((d,i)=> pad.l + i*((w-pad.l-pad.r)/(data.length-1 || 1)));
const yVals = data.map(d=>d.value);
const yMin = Math.min(...yVals) - 1, yMax = Math.max(...yVals) + 1;
const yScale = linearScale(yMin,yMax,h-pad.b,pad.t);
// axes lines & labels
const yAxis = document.createElementNS("http://www.w3.org/2000/svg",'g');
for(let i=0;i<=4;i++){
const val = yMin + (i/4)*(yMax-yMin);
const y = yScale(val);
const line = document.createElementNS("http://www.w3.org/2000/svg",'line');
line.setAttribute('x1',pad.l);line.setAttribute('x2',w-pad.r);
line.setAttribute('y1',y);line.setAttribute('y2',y);
line.setAttribute('stroke','rgba(255,255,255,0.03)');
yAxis.appendChild(line);
const txt = document.createElementNS("http://www.w3.org/2000/svg",'text');
txt.setAttribute('x',10);txt.setAttribute('y',y+4);
txt.setAttribute('fill','var(--muted)');
txt.setAttribute('font-size',12);
txt.textContent = format(val,2) + '%';
yAxis.appendChild(txt);
}
svg.appendChild(yAxis);
// bars
data.forEach((d,i)=>{
const barW = 28;
const x = xs[i] - barW/2;
const y = yScale(d.value);
const height = h - pad.b - y;
const rect = document.createElementNS("http://www.w3.org/2000/svg",'rect');
rect.setAttribute('x',x); rect.setAttribute('y',y); rect.setAttribute('width',barW); rect.setAttribute('height',height);
rect.setAttribute('fill','rgba(6,182,212,0.25)');
rect.setAttribute('rx',6);
svg.appendChild(rect);
rect.addEventListener('mousemove', (ev)=> showTip(`${d.period}<br><strong>${d.value}%</strong>`, ev.clientX, ev.clientY));
rect.addEventListener('mouseleave', hideTip);
});
// line path
const pathD = data.map((d,i)=>{
const x = xs[i]; const y = yScale(d.value);
return (i===0 ? `M ${x} ${y}` : `L ${x} ${y}`);
}).join(' ');
const path = document.createElementNS("http://www.w3.org/2000/svg",'path');
path.setAttribute('d', pathD);
path.setAttribute('fill','none');
path.setAttribute('stroke','url(#gline)');
path.setAttribute('stroke-width',3);
path.setAttribute('stroke-linecap','round');
path.setAttribute('stroke-linejoin','round');
// gradient
const defs = document.createElementNS("http://www.w3.org/2000/svg",'defs');
defs.innerHTML = `<linearGradient id="gline" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="${getComputedStyle(document.documentElement).getPropertyValue('--accent').trim()}" /><stop offset="1" stop-color="${getComputedStyle(document.documentElement).getPropertyValue('--accent-2').trim()}" /></linearGradient>`;
svg.appendChild(defs);
svg.appendChild(path);
// points
data.forEach((d,i)=>{
const x = xs[i]; const y = yScale(d.value);
const circle = document.createElementNS("http://www.w3.org/2000/svg",'circle');
circle.setAttribute('cx',x); circle.setAttribute('cy',y); circle.setAttribute('r',6);
circle.setAttribute('fill','#061426');
circle.setAttribute('stroke','url(#gline)');
circle.setAttribute('stroke-width',3);
svg.appendChild(circle);
circle.addEventListener('mousemove',(ev)=> showTip(`${d.period}<br><strong>${d.value}%</strong>`,ev.clientX,ev.clientY));
circle.addEventListener('mouseleave', hideTip);
});
// x labels
data.forEach((d,i)=>{
const x = xs[i];
const txt = document.createElementNS("http://www.w3.org/2000/svg",'text');
txt.setAttribute('x',x); txt.setAttribute('y', h - 8);
txt.setAttribute('text-anchor','middle');
txt.setAttribute('fill','var(--muted)');
txt.setAttribute('font-size',12);
txt.textContent = d.period.replace('2025 ','');
svg.appendChild(txt);
});
}
/* ========= Annual Bar Chart ========= */
function drawGPDA(){
const svg = q('#svgGPDA');
svg.innerHTML = '';
const data = DATA.gdpAnnual;
const w=800,h=300,pad={l:48,r:20,t:30,b:40};
const xs = data.map((d,i)=> pad.l + i*((w-pad.l-pad.r)/(data.length-1 || 1)));
const yVals = data.map(d=>d.value);
const yMin = Math.min(...yVals) - 1, yMax = Math.max(...yVals)+1;
const yScale = linearScale(yMin,yMax,h-pad.b,pad.t);
// bars & labels
data.forEach((d,i)=>{
const barW = 40;
const x = xs[i] - barW/2;
const y = yScale(d.value);
const height = h - pad.b - y;
const rect = document.createElementNS("http://www.w3.org/2000/svg",'rect');
rect.setAttribute('x',x); rect.setAttribute('y',y); rect.setAttribute('width',barW); rect.setAttribute('height',height);
rect.setAttribute('rx',6);
rect.setAttribute('fill', i===data.length-1 ? 'url(#gbar)' : 'rgba(124,58,237,0.6)');
svg.appendChild(rect);
rect.addEventListener('mousemove',(ev)=> showTip(`${d.year}<br><strong>${d.value}%</strong>`,ev.clientX,ev.clientY));
rect.addEventListener('mouseleave', hideTip);
const txt = document.createElementNS("http://www.w3.org/2000/svg",'text');
txt.setAttribute('x',x+barW/2); txt.setAttribute('y',h-8); txt.setAttribute('text-anchor','middle');
txt.setAttribute('fill','var(--muted)');
txt.textContent = d.year;
svg.appendChild(txt);
});
const defs = document.createElementNS("http://www.w3.org/2000/svg",'defs');
defs.innerHTML = `<linearGradient id="gbar" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="${getComputedStyle(document.documentElement).getPropertyValue('--accent-2').trim()}" /><stop offset="1" stop-color="${getComputedStyle(document.documentElement).getPropertyValue('--accent').trim()}" /></linearGradient>`;
svg.appendChild(defs);
}
/* ========= Inflation Chart (bars & forecast markers) ========= */
function drawInflation(){
const svg = q('#svgInfl');
svg.innerHTML = '';
const data = DATA.inflation;
const w=800,h=220,pad={l:60,r:20,t:20,b:40};
const xs = data.map((d,i)=> pad.l + i*((w-pad.l-pad.r)/(data.length-1 || 1)));
const yVals = data.map(d=>d.value);
const yMin = 0, yMax = Math.max(...yVals)+1;
const yScale = linearScale(yMin,yMax,h-pad.b,pad.t);
// bars for May/June, markers for forecasts
data.forEach((d,i)=>{
const x = xs[i];
if(d.period.includes('Forecast')){
const cx = x;
const cy = yScale(d.value);
const circle = document.createElementNS("http://www.w3.org/2000/svg",'circle');
circle.setAttribute('cx',cx); circle.setAttribute('cy',cy); circle.setAttribute('r',8);
circle.setAttribute('fill',d.period.includes('IMF') ? '#f97316' : '#60a5fa');
svg.appendChild(circle);
circle.addEventListener('mousemove',(ev)=> showTip(`${d.period}<br><strong>${d.value}%</strong>`,ev.clientX,ev.clientY));
circle.addEventListener('mouseleave', hideTip);
} else {
const barW = 40;
const y = yScale(d.value);
const height = h - pad.b - y;
const rect = document.createElementNS("http://www.w3.org/2000/svg",'rect');
rect.setAttribute('x',x - barW/2); rect.setAttribute('y',y); rect.setAttribute('width',barW); rect.setAttribute('height',height);
rect.setAttribute('fill','rgba(124,58,237,0.5)');
rect.setAttribute('rx',8);
svg.appendChild(rect);
rect.addEventListener('mousemove',(ev)=> showTip(`${d.period}<br><strong>${d.value}%</strong>`,ev.clientX,ev.clientY));
rect.addEventListener('mouseleave', hideTip);
}
const txt = document.createElementNS("http://www.w3.org/2000/svg",'text');
txt.setAttribute('x',x); txt.setAttribute('y',h-8); txt.setAttribute('text-anchor','middle'); txt.setAttribute('fill','var(--muted)');
txt.setAttribute('font-size',12);
txt.textContent = d.period.replace(' 2025','').replace('Forecast ','');
svg.appendChild(txt);
});
}
/* ========= Sector Pie Chart ========= */
function drawSectorPie(){
const svg = q('#svgSector');
svg.innerHTML = '';
const w=800,h=300,cx=w/2,cy=h/2,r=90;
const total = DATA.sectors.reduce((s,el)=>s+el.share,0);
let startAngle = -Math.PI/2;
DATA.sectors.forEach(s=>{
const slice = (s.share/total) * Math.PI*2;
const end = startAngle + slice;
const x1 = cx + r*Math.cos(startAngle), y1 = cy + r*Math.sin(startAngle);
const x2 = cx + r*Math.cos(end), y2 = cy + r*Math.sin(end);
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 ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z`;
path.setAttribute('d', d);
path.setAttribute('fill', s.color);
path.setAttribute('stroke','rgba(0,0,0,0.15)');
path.setAttribute('stroke-width',1);
svg.appendChild(path);
path.addEventListener('mousemove',(ev)=> showTip(`${s.name}<br><strong>${s.share}%</strong>`,ev.clientX,ev.clientY));
path.addEventListener('mouseleave', hideTip);
startAngle = end;
});
// legend
const legend = q('#legendSector');
legend.innerHTML = '';
DATA.sectors.forEach(s=>{
const sp = document.createElement('span');
sp.innerHTML = `<i style="background:${s.color}"></i> ${s.name} <strong style="margin-left:6px">${s.share}%</strong>`;
legend.appendChild(sp);
});
}
/* ========= Scatter (FDI vs GDP) ========= */
function drawScatter(){
const svg = q('#svgScatter');
svg.innerHTML = '';
const data = DATA.regionsFDI;
const w=800,h=300,pad={l:60,r:40,t:20,b:40};
const xVals = data.map(d=>d.fdi), yVals = data.map(d=>d.gdpGrowth);
const xMin = 0, xMax = Math.max(...xVals)+2, yMin = Math.min(...yVals)-1, yMax = Math.max(...yVals)+1;
const xScale = linearScale(xMin,xMax,pad.l,w-pad.r);
const yScale = linearScale(yMin,yMax,h-pad.b,pad.t);
// axes grid
for(let i=0;i<=4;i++){
const y = yScale(yMin + (i/4)*(yMax-yMin));
const line = document.createElementNS("http://www.w3.org/2000/svg",'line');
line.setAttribute('x1',pad.l); line.setAttribute('x2',w-pad.r); line.setAttribute('y1',y); line.setAttribute('y2',y);
line.setAttribute('stroke','rgba(255,255,255,0.03)');
svg.appendChild(line);
}
// points
data.forEach(d=>{
const cx = xScale(d.fdi);
const cy = yScale(d.gdpGrowth);
const g = document.createElementNS("http://www.w3.org/2000/svg",'g');
const circle = document.createElementNS("http://www.w3.org/2000/svg",'circle');
circle.setAttribute('cx',cx); circle.setAttribute('cy',cy); circle.setAttribute('r',10);
circle.setAttribute('fill','rgba(124,58,237,0.85)');
circle.setAttribute('opacity',0.95);
const txt = document.createElementNS("http://www.w3.org/2000/svg",'text');
txt.setAttribute('x',cx+14); txt.setAttribute('y',cy+4); txt.setAttribute('fill','var(--muted)'); txt.textContent = d.region;
g.appendChild(circle); g.appendChild(txt);
svg.appendChild(g);
g.addEventListener('mousemove',(ev)=> showTip(`${d.region}<br>FDI: ${d.fdi}B<br>GDP Growth: ${d.gdpGrowth}%`,ev.clientX,ev.clientY));
g.addEventListener('mouseleave', hideTip);
});
// axes labels
const xlabel = document.createElementNS("http://www.w3.org/2000/svg",'text');
xlabel.setAttribute('x',w/2); xlabel.setAttribute('y',h-6); xlabel.setAttribute('text-anchor','middle'); xlabel.setAttribute('fill','var(--muted)');
xlabel.textContent = 'FDI (US$B)';
svg.appendChild(xlabel);
const ylabel = document.createElementNS("http://www.w3.org/2000/svg",'text');
ylabel.setAttribute('x',14); ylabel.setAttribute('y',20); ylabel.setAttribute('fill','var(--muted)');
ylabel.textContent = 'GDP Growth %';
svg.appendChild(ylabel);
}
/* ========= Heatmap ========= */
function drawHeatmap(){
const container = q('#heatmap');
container.innerHTML = '';
const m = DATA.monthly.months;
const maxInfl = Math.max(...DATA.monthly.inflation);
const maxFdi = Math.max(...DATA.monthly.fdiMonthlyB);
const maxRetail = Math.max(...DATA.monthly.retailIndex);
// we'll create 3 rows: inflation, fdi, retail
const rows = [
{label:'Inflation %', values:DATA.monthly.inflation, max:maxInfl, color:'#f97316'},
{label:'Monthly FDI (US$B)', values:DATA.monthly.fdiMonthlyB, max:maxFdi, color:'#06b6d4'},
{label:'Retail Index', values:DATA.monthly.retailIndex, max:maxRetail, color:'#7c3aed'}
];
rows.forEach(row=>{
// header cell
const head = document.createElement('div');
head.className='heat-cell';
head.style.background='transparent';
head.style.alignItems='center';
head.style.justifyContent='flex-start';
head.style.fontWeight='600';
head.style.color='var(--muted)';
head.textContent = row.label;
head.style.gridColumn = 'span 2';
container.appendChild(head);
// months
row.values.forEach((v,i)=>{
const cell = document.createElement('div');
cell.className='heat-cell';
const intensity = v/row.max;
const base = hexToRgb(row.color);
const bg = `rgba(${base.r},${base.g},${base.b},${0.2+intensity*0.75})`;
cell.style.background = bg;
cell.textContent = v;
cell.title = `${row.label} ${m[i]}: ${v}`;
cell.addEventListener('mouseenter', (ev)=> showTip(`${row.label}${m[i]}<br><strong>${v}</strong>`, ev.clientX, ev.clientY));
cell.addEventListener('mouseleave', hideTip);
container.appendChild(cell);
});
});
}
function hexToRgb(hex){
const c = hex.replace('#','');
return { r: parseInt(c.substring(0,2),16), g: parseInt(c.substring(2,4),16), b: parseInt(c.substring(4,6),16) };
}
/* ========= Legend & Interactivity ========= */
function initLegends(){
q('#legendGDPQ').innerHTML = `<span><i style="background:linear-gradient(90deg,var(--accent),var(--accent-2))"></i> Line</span>`;
q('#legendGPDA').innerHTML = `<span><i style="background:var(--accent-2)"></i> Annual</span>`;
q('#legendInfl').innerHTML = `<span><i style="background:rgba(124,58,237,0.6)"></i> May/June</span><span><i style="background:#f97316"></i> IMF</span><span><i style="background:#60a5fa"></i> ADB</span>`;
q('#legendScatter').innerHTML = `<span class="muted">Regions (size ~ relative)</span>`;
}
/* ========= Render everything initially and on visibility ========= */
function renderAll(){
drawGDPQuarter();
drawGPDA();
drawInflation();
drawSectorPie();
drawScatter();
drawHeatmap();
initLegends();
}
renderAll();
/* Animate on scroll into view using IntersectionObserver */
const io = new IntersectionObserver((entries)=>{
entries.forEach(en=>{
if(en.isIntersecting){
const id = en.target.id;
if(id==='visualizations' || id==='indicators'){
triggerChartDraws();
}
}
});
}, {threshold:0.2});
['indicators','visualizations','table','sectoral','appendix'].forEach(id=>{
const el = document.getElementById(id);
if(el) io.observe(el);
});
function triggerChartDraws(){
// small animation: update KPIs from dataset
q('#kpiGdp').animate([{opacity:0},{opacity:1}],{duration:600});
q('#kpiGdp').textContent = DATA.gdpAnnual[DATA.gdpAnnual.length-1].value + '%';
}
/* ========= Export SVG to PNG ========= */
q('#exportPNG').addEventListener('click', async ()=>{
const svgEl = document.querySelector('#svgGPDA') || document.querySelector('svg');
const serializer = new XMLSerializer();
const source = serializer.serializeToString(svgEl);
const svgBlob = new Blob([source], {type:'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(svgBlob);
const img = new Image();
img.onload = function(){
const canvas = document.createElement('canvas');
canvas.width = img.width; canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.fillStyle = getComputedStyle(document.body).backgroundColor || '#071021';
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.drawImage(img,0,0);
URL.revokeObjectURL(url);
canvas.toBlob(function(blob){
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'chart.png';
link.click();
});
};
img.src = url;
});
/* ========= TOC interactions & progress indicator ========= */
const tocLinks = qq('.toc a');
tocLinks.forEach(a=>{
a.addEventListener('click', (e)=>{
tocLinks.forEach(x=>x.classList.remove('active'));
a.classList.add('active');
// smooth scroll handled by CSS scroll-behavior
});
});
// highlight on scroll
const sections = Array.from(document.querySelectorAll('main > section, main > header'));
const secObserver = new IntersectionObserver((entries)=>{
entries.forEach(en=>{
const id = en.target.id;
const link = document.querySelector(`.toc a[data-target="${id}"]`);
if(link){
if(en.isIntersecting) link.classList.add('active');
else link.classList.remove('active');
}
});
// overall progress
const visible = sections.filter(s=> s.getBoundingClientRect().top < window.innerHeight*0.6 && s.getBoundingClientRect().bottom > window.innerHeight*0.2);
const idx = Math.max(0, Math.min(sections.indexOf(visible[0]) || 0, sections.length-1));
const pct = Math.round(((idx+1)/sections.length)*100);
q('#overallProgress').style.width = pct + '%';
}, {threshold:0.35});
sections.forEach(s=>secObserver.observe(s));
/* ========= Search TOC filter ========= */
q('#tocFilter').addEventListener('input', (e)=>{
const qv = e.target.value.toLowerCase();
qq('.toc-content a').forEach(a=>{
a.style.display = a.textContent.toLowerCase().includes(qv) ? 'flex' : 'none';
});
});
/* ========= Filter series and reset ========= */
q('#filterSeries').addEventListener('input', (e)=>{
const qv = e.target.value.toLowerCase();
// filter sectors pie by name
if(!qv){
drawSectorPie();
} else {
const filtered = DATA.sectors.filter(s => s.name.toLowerCase().includes(qv));
if(filtered.length>0){
// temporarily draw simple pie
const svg = q('#svgSector');
svg.innerHTML = '';
const w=800,h=300,cx=w/2,cy=h/2,r=90;
const total = filtered.reduce((s,el)=>s+el.share,0);
let startAngle = -Math.PI/2;
filtered.forEach(s=>{
const slice = (s.share/total) * Math.PI*2;
const end = startAngle + slice;
const x1 = cx + r*Math.cos(startAngle), y1 = cy + r*Math.sin(startAngle);
const x2 = cx + r*Math.cos(end), y2 = cy + r*Math.sin(end);
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 ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z`;
path.setAttribute('d', d);
path.setAttribute('fill', s.color);
svg.appendChild(path);
startAngle = end;
});
}
}
});
q('#resetView').addEventListener('click', ()=>{
q('#filterSeries').value='';
drawSectorPie();
drawGDPQuarter();
drawGPDA();
drawInflation();
drawScatter();
drawHeatmap();
});
/* ========= Small UX touches ========= */
// copy table on double click row
tableBody.addEventListener('dblclick', async (e)=>{
const tr = e.target.closest('tr');
if(!tr) return;
const txt = Array.from(tr.children).map(td=>td.textContent).join('\t');
await navigator.clipboard.writeText(txt);
flashButton(q('#copyTable'),'Row copied');
});
// localStorage remember last table search
q('#tableSearch').value = localStorage.getItem('tableSearch') || '';
q('#tableSearch').addEventListener('input', (e)=> localStorage.setItem('tableSearch', e.target.value));
// small keyboard shortcut: "/" focus table search
window.addEventListener('keydown', (e)=>{
if(e.key === '/') { e.preventDefault(); q('#tableSearch').focus(); }
});
// enable opening scatter focus
q('#openScatter').addEventListener('click', ()=> {
document.getElementById('chartScatter').scrollIntoView({behavior:'smooth', block:'center'});
});
/* ========= Accessibility & small helpers ========= */
// set ARIA labels on interactive elements
qq('.chart').forEach(c=> c.setAttribute('role','region'));
qq('.btn').forEach(b=> b.setAttribute('role','button'));
// initial active toc
const first = document.querySelector('.toc a');
if(first) first.classList.add('active');
// small debounce helper
function debounce(fn, wait=200){
let t;
return (...args)=> { clearTimeout(t); t = setTimeout(()=>fn(...args), wait); };
}
// window resize => re-render SVG charts for responsive
window.addEventListener('resize', debounce(()=>{ renderAll(); }, 200));
</script>
</body>
</html>