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