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> |