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> | |
| <!-- Font Awesome for icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-yH6fK7Y4W5r8Xwq5mZ4mN6kJm3rQ0y7s8gqO2x1Q7s6Rz5yG3qk9qE5a7Y6vN2uB4t8vZ3F3p2j9h6w==" | |
| crossorigin="anonymous" referrerpolicy="no-referrer" /> | |
| <style> | |
| :root{ | |
| --bg: #f7f9fb; | |
| --card: #ffffff; | |
| --muted: #6b7280; | |
| --accent: linear-gradient(135deg,#0ea5a2 0%,#06b6d4 100%); | |
| --primary: #0b7285; | |
| --success: #16a34a; | |
| --danger: #ef4444; | |
| --glass: rgba(255,255,255,0.6); | |
| --radius: 14px; | |
| --max-width: 1200px; | |
| --shadow: 0 6px 24px rgba(12,18,31,0.08); | |
| --kb: 14px; | |
| --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; | |
| } | |
| /* Theme toggling */ | |
| :root[data-theme='dark']{ | |
| --bg: #071022; | |
| --card: #0b1623; | |
| --muted: #9aa6b2; | |
| --primary: #2ad4e0; | |
| --glass: rgba(255,255,255,0.03); | |
| color-scheme: dark; | |
| } | |
| /* Global styles */ | |
| *{box-sizing:border-box} | |
| html,body{height:100%;} | |
| body{ | |
| margin:0; | |
| background: radial-gradient(1200px 400px at 10% 10%, rgba(6,182,212,0.06), transparent), var(--bg); | |
| font-family:var(--font-sans); | |
| color:var(--muted); | |
| -webkit-font-smoothing:antialiased; | |
| -moz-osx-font-smoothing:grayscale; | |
| line-height:1.45; | |
| font-size:clamp(14px, 1.4vw, 16px); | |
| padding:24px; | |
| } | |
| .container{ | |
| max-width:var(--max-width); | |
| margin:0 auto; | |
| display:grid; | |
| grid-template-columns: 1fr; | |
| gap:20px; | |
| } | |
| header.site-header{ | |
| display:flex; | |
| gap:12px; | |
| align-items:center; | |
| justify-content:space-between; | |
| } | |
| .brand{ | |
| display:flex; | |
| gap:12px; | |
| align-items:center; | |
| } | |
| .logo{ | |
| width:56px; | |
| height:56px; | |
| border-radius:12px; | |
| background:var(--accent); | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| color:white; | |
| font-weight:700; | |
| box-shadow:var(--shadow); | |
| flex:0 0 56px; | |
| } | |
| h1{ | |
| margin:0; | |
| color:var(--primary); | |
| font-size:clamp(18px,2.6vw,22px); | |
| } | |
| p.lead{ | |
| margin:0; | |
| color:var(--muted); | |
| font-size:clamp(12px,1.4vw,14px); | |
| } | |
| .controls{ | |
| display:flex; | |
| gap:10px; | |
| align-items:center; | |
| } | |
| .btn{ | |
| background:var(--card); | |
| border-radius:10px; | |
| padding:10px 12px; | |
| box-shadow:var(--shadow); | |
| border:1px solid rgba(0,0,0,0.04); | |
| color:var(--muted); | |
| cursor:pointer; | |
| display:inline-flex; | |
| align-items:center; | |
| gap:8px; | |
| font-size:13px; | |
| } | |
| .btn.icon{padding:8px} | |
| .btn.primary{ | |
| background:linear-gradient(180deg, rgba(6,182,212,0.14), rgba(6,182,212,0.04)); | |
| color:var(--primary); | |
| border:1px solid rgba(6,182,212,0.18); | |
| } | |
| /* Layout for main content with TOC sidebar */ | |
| .layout{ | |
| display:grid; | |
| gap:18px; | |
| grid-template-columns: 1fr; | |
| } | |
| /* TOC */ | |
| nav.toc{ | |
| position:sticky; | |
| top:24px; | |
| align-self:start; | |
| background:var(--card); | |
| border-radius:12px; | |
| padding:12px; | |
| box-shadow:var(--shadow); | |
| display:flex; | |
| flex-direction:column; | |
| gap:10px; | |
| } | |
| .toc .search{ | |
| display:flex; | |
| gap:8px; | |
| align-items:center; | |
| } | |
| .toc input[type="search"]{ | |
| border:0; | |
| outline:0; | |
| background:var(--glass); | |
| padding:8px 10px; | |
| border-radius:8px; | |
| width:100%; | |
| color:var(--muted); | |
| font-size:13px; | |
| } | |
| .toc .links{ | |
| display:flex; | |
| flex-direction:column; | |
| gap:6px; | |
| margin-top:6px; | |
| } | |
| .toc a{ | |
| text-decoration:none; | |
| color:var(--muted); | |
| padding:8px; | |
| border-radius:8px; | |
| font-size:13px; | |
| display:flex; | |
| gap:8px; | |
| align-items:center; | |
| } | |
| .toc a.active{ | |
| background:linear-gradient(90deg, rgba(6,182,212,0.08), rgba(6,182,212,0.02)); | |
| color:var(--primary); | |
| font-weight:600; | |
| } | |
| .toc .progress-vert{ | |
| height:6px; | |
| background:linear-gradient(90deg,#e6fdfd,transparent); | |
| border-radius:99px; | |
| overflow:hidden; | |
| margin-top:6px; | |
| } | |
| .progress-vert > i{ | |
| display:block; | |
| height:100%; | |
| width:0%; | |
| background:linear-gradient(90deg,#06b6d4,#0ea5a2); | |
| } | |
| /* Main content area */ | |
| main.content{ | |
| display:grid; | |
| gap:18px; | |
| } | |
| .card{ | |
| background:var(--card); | |
| border-radius:var(--radius); | |
| padding:16px; | |
| box-shadow:var(--shadow); | |
| border:1px solid rgba(0,0,0,0.04); | |
| } | |
| /* Executive summary */ | |
| .summary{ | |
| display:flex; | |
| flex-direction:column; | |
| gap:12px; | |
| } | |
| .summary .kpi-row{ | |
| display:flex; | |
| gap:12px; | |
| flex-wrap:wrap; | |
| } | |
| .kpi{ | |
| flex:1 1 160px; | |
| background:linear-gradient(180deg, rgba(6,182,212,0.06), rgba(6,182,212,0.02)); | |
| border-radius:10px; | |
| padding:12px; | |
| min-width:140px; | |
| } | |
| .kpi h3{margin:0;color:var(--primary);font-size:clamp(16px,2.2vw,18px)} | |
| .kpi p{margin:6px 0 0 0;color:var(--muted);font-weight:600} | |
| /* Indicators grid */ | |
| .indicators-grid{ | |
| display:grid; | |
| gap:12px; | |
| grid-template-columns: repeat(auto-fit,minmax(220px,1fr)); | |
| } | |
| /* Charts area */ | |
| .charts{ | |
| display:grid; | |
| gap:12px; | |
| grid-template-columns: 1fr; | |
| } | |
| .chart{ | |
| height:260px; | |
| aspect-ratio: 16 / 9; | |
| position:relative; | |
| overflow:visible; | |
| } | |
| .chart svg{width:100%;height:100%} | |
| .tooltip{ | |
| position:fixed; | |
| pointer-events:none; | |
| background:var(--card); | |
| color:var(--muted); | |
| padding:8px 10px; | |
| border-radius:8px; | |
| box-shadow:var(--shadow); | |
| font-size:13px; | |
| border:1px solid rgba(0,0,0,0.04); | |
| transform:translate(-50%,-120%); | |
| white-space:nowrap; | |
| z-index:9999; | |
| display:none; | |
| } | |
| /* Sortable table */ | |
| table{ | |
| width:100%; | |
| border-collapse:collapse; | |
| font-size:13px; | |
| } | |
| thead th{ | |
| text-align:left; | |
| padding:10px; | |
| background:transparent; | |
| color:var(--muted); | |
| font-weight:700; | |
| cursor:pointer; | |
| } | |
| tbody td{ | |
| padding:10px; | |
| border-top:1px solid rgba(0,0,0,0.04); | |
| color:var(--muted); | |
| } | |
| tbody tr:hover td{background:rgba(6,182,212,0.03)} | |
| /* Collapsible lists */ | |
| details summary{ | |
| list-style:none; | |
| cursor:pointer; | |
| display:flex; | |
| gap:10px; | |
| align-items:center; | |
| font-weight:700; | |
| color:var(--primary); | |
| } | |
| details[open] summary .chev{transform:rotate(180deg)} | |
| /* References */ | |
| .references li{margin-bottom:8px} | |
| .citation{ | |
| display:flex; | |
| gap:8px; | |
| align-items:center; | |
| font-size:13px; | |
| color:var(--muted); | |
| } | |
| .badge{ | |
| background:linear-gradient(90deg,#06b6d4,#0ea5a2); | |
| color:white; | |
| padding:6px 8px; | |
| border-radius:8px; | |
| font-weight:700; | |
| font-size:12px; | |
| } | |
| /* Responsive layout adjustments */ | |
| @media (min-width:768px){ | |
| .layout{grid-template-columns: 260px 1fr; align-items:start} | |
| .container{grid-template-columns: 1fr} | |
| .charts{grid-template-columns: 1fr 1fr} | |
| } | |
| @media (min-width:1024px){ | |
| .container{padding:0} | |
| .charts{grid-template-columns: 2fr 1fr} | |
| .chart.large{aspect-ratio: 21/9; height:340px} | |
| } | |
| /* Print-friendly */ | |
| @media print{ | |
| .controls, nav.toc, .btn, .tooltip{display:none} | |
| body{background:white;padding:0} | |
| } | |
| /* Container query example for a card's content */ | |
| .card:where(:not(:root)){ container-type: inline-size;} | |
| @container (min-width:420px){ | |
| .card .meta-row{display:flex;justify-content:space-between;align-items:center} | |
| } | |
| /* small utilities */ | |
| .muted{color:var(--muted)} | |
| .small{font-size:12px} | |
| .right{text-align:right} | |
| .center{text-align:center} | |
| .flex{display:flex} | |
| .gap-8{gap:8px} | |
| .pill{padding:6px 10px;border-radius:999px;background:var(--glass);font-weight:700;color:var(--muted)} | |
| .legend{display:flex;flex-wrap:wrap;gap:8px;align-items:center} | |
| .legend-item{display:flex;gap:8px;align-items:center;font-size:13px} | |
| .swatch{width:14px;height:14px;border-radius:4px} | |
| footer{opacity:0.9;color:var(--muted);font-size:13px;text-align:center;padding:12px 0} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header class="site-header"> | |
| <div class="brand"> | |
| <div class="logo">VN</div> | |
| <div> | |
| <h1>Vietnam Economic Growth Report — 2025</h1> | |
| <p class="lead">Interactive analysis of recent performance, outlook and risks</p> | |
| </div> | |
| </div> | |
| <div class="controls" role="toolbar" aria-label="Controls"> | |
| <button class="btn" id="copySummary" title="Copy Executive Summary"><i class="fa fa-copy"></i> Copy Summary</button> | |
| <button class="btn" id="exportCSV" title="Export indicators as CSV"><i class="fa fa-file-csv"></i> Export CSV</button> | |
| <button class="btn" id="exportJSON" title="Export report as JSON"><i class="fa fa-code"></i> Export JSON</button> | |
| <button class="btn" id="printBtn" title="Print view"><i class="fa fa-print"></i></button> | |
| <button class="btn icon" id="themeToggle" title="Toggle theme"><i class="fa fa-moon"></i></button> | |
| </div> | |
| </header> | |
| <div class="layout"> | |
| <!-- TOC Sidebar --> | |
| <nav class="toc card" aria-label="Table of contents"> | |
| <div class="search"> | |
| <i class="fa fa-search" style="color:var(--muted)"></i> | |
| <input type="search" id="globalSearch" placeholder="Search report..." aria-label="Search report"/> | |
| </div> | |
| <div class="links" id="tocLinks"> | |
| <a href="#executive" data-id="executive"><i class="fa fa-star"></i> Executive Summary</a> | |
| <a href="#indicators" data-id="indicators"><i class="fa fa-chart-line"></i> Key Indicators</a> | |
| <a href="#sectors" data-id="sectors"><i class="fa fa-industry"></i> Sectoral Analysis</a> | |
| <a href="#fdi" data-id="fdi"><i class="fa fa-hand-holding-dollar"></i> FDI & Capital</a> | |
| <a href="#history" data-id="history"><i class="fa fa-history"></i> Historical Comparison</a> | |
| <a href="#outlook" data-id="outlook"><i class="fa fa-compass"></i> Economic Outlook</a> | |
| <a href="#risks" data-id="risks"><i class="fa fa-exclamation-triangle"></i> Challenges & Risks</a> | |
| <a href="#references" data-id="references"><i class="fa fa-book"></i> References</a> | |
| </div> | |
| <div class="progress-vert" aria-hidden="true"> | |
| <i id="tocProgress"></i> | |
| </div> | |
| <div style="display:flex;gap:8px;margin-top:8px"> | |
| <button class="btn" id="saveBookmark" title="Save current section"><i class="fa fa-bookmark"></i> Save</button> | |
| <button class="btn" id="gotoBookmark" title="Open saved section"><i class="fa fa-location-arrow"></i> Go</button> | |
| </div> | |
| </nav> | |
| <!-- Content --> | |
| <main class="content"> | |
| <!-- Executive Summary --> | |
| <section id="executive" class="card" tabindex="0" aria-labelledby="executiveTitle"> | |
| <div class="meta-row"> | |
| <div> | |
| <h2 id="executiveTitle" style="margin:0;color:var(--primary)">Executive Summary</h2> | |
| <p class="small muted">Snapshot of growth, drivers and immediate outlook</p> | |
| </div> | |
| <div class="pill small">Last updated: 2025</div> | |
| </div> | |
| <div class="summary"> | |
| <p class="muted">Vietnam's economy sustained strong momentum in 2025, achieving 7.52% growth in H1 and 7.96% YoY in Q2. The expansion is led by services and manufacturing, supported by robust FDI inflows and resilient domestic demand despite elevated global uncertainty.</p> | |
| <div class="kpi-row"> | |
| <div class="kpi"> | |
| <h3>7.52%</h3> | |
| <p>GDP Growth H1 2025 (highest H1 since 2011)</p> | |
| </div> | |
| <div class="kpi"> | |
| <h3>7.96%</h3> | |
| <p>GDP Growth Q2 2025 YoY</p> | |
| </div> | |
| <div class="kpi"> | |
| <h3>3.57%</h3> | |
| <p>Inflation (June 2025)</p> | |
| </div> | |
| <div class="kpi"> | |
| <h3>US$21.51B</h3> | |
| <p>FDI (First Half 2025)</p> | |
| </div> | |
| </div> | |
| <div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:6px"> | |
| <button class="btn primary" id="readMoreExec"><i class="fa fa-eye"></i> Expand Summary</button> | |
| <div class="muted small">Use the table of contents to navigate sections. Use search to find keywords.</div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Key Indicators --> | |
| <section id="indicators" class="card" tabindex="0" aria-labelledby="indicatorsTitle"> | |
| <h2 id="indicatorsTitle" style="margin:0;color:var(--primary)">Key Economic Indicators</h2> | |
| <p class="small muted">Core datasets and comparisons for 2025</p> | |
| <div class="indicators-grid" style="margin-top:12px"> | |
| <div class="card" style="padding:12px"> | |
| <h3 style="margin:0;color:var(--primary)">GDP Series</h3> | |
| <p class="small muted">Quarterly / mid-year performance</p> | |
| <div style="display:flex;gap:8px;align-items:center;margin-top:10px;flex-wrap:wrap"> | |
| <div><strong>Q1 2025</strong><div class="muted small">6.9% YoY</div></div> | |
| <div><strong>Q2 2025</strong><div class="muted small">7.96% YoY</div></div> | |
| <div><strong>H1 2025</strong><div class="muted small">7.52% YoY</div></div> | |
| </div> | |
| </div> | |
| <div class="card" style="padding:12px"> | |
| <h3 style="margin:0;color:var(--primary)">Inflation & Labor</h3> | |
| <p class="small muted">Key macro indicators</p> | |
| <div style="display:flex;gap:12px;align-items:center;margin-top:10px"> | |
| <div><strong>Inflation</strong><div class="muted small">May: 3.24% • Jun: 3.57%</div></div> | |
| <div><strong>Unemployment</strong><div class="muted small">Q1 2025: 2.20%</div></div> | |
| </div> | |
| </div> | |
| <div class="card" style="padding:12px"> | |
| <h3 style="margin:0;color:var(--primary)">FDI</h3> | |
| <p class="small muted">Capital flows and confidence</p> | |
| <div style="margin-top:10px"> | |
| <div><strong>Registered (Jan–May)</strong><div class="muted small">US$18.4B (+51% YoY)</div></div> | |
| <div style="margin-top:6px"><strong>Disbursed (Jan–May)</strong><div class="muted small">US$8.9B</div></div> | |
| </div> | |
| </div> | |
| <div class="card" style="padding:12px"> | |
| <h3 style="margin:0;color:var(--primary)">Forecasts</h3> | |
| <p class="small muted">Cross-institution projections (2025)</p> | |
| <ul style="margin:8px 0 0 16px" class="muted small"> | |
| <li>World Bank: 5.8%</li> | |
| <li>ADB: 6.6%</li> | |
| <li>IMF: 5.2%</li> | |
| <li>Government Target: 8.3–8.5%</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <!-- Charts --> | |
| <div class="charts" style="margin-top:14px"> | |
| <div class="card chart large" id="chartHistoryHolder"> | |
| <h4 style="margin:0;color:var(--primary)">Historical GDP Growth (2020–2025 Q1 series)</h4> | |
| <p class="small muted">Year-on-year growth in first quarter (2020–2025)</p> | |
| <div id="chartHistory" class="chart" aria-hidden="false"></div> | |
| </div> | |
| <div class="card chart" id="chartForecastHolder"> | |
| <h4 style="margin:0;color:var(--primary)">2025 Growth Forecasts</h4> | |
| <p class="small muted">Institutional consensus vs Government target</p> | |
| <div id="chartForecast" class="chart"></div> | |
| <div style="margin-top:8px" class="legend" id="forecastLegend"></div> | |
| </div> | |
| </div> | |
| <!-- Sortable table --> | |
| <div class="card" style="margin-top:12px"> | |
| <div style="display:flex;justify-content:space-between;align-items:center"> | |
| <h4 style="margin:0;color:var(--primary)">Indicator Table (sortable)</h4> | |
| <div class="small muted">Click headers to sort • Use export buttons above</div> | |
| </div> | |
| <div style="margin-top:10px;overflow:auto"> | |
| <table id="indicatorTable" aria-label="Key indicators table"> | |
| <thead> | |
| <tr> | |
| <th data-key="indicator">Indicator <i class="fa fa-sort"></i></th> | |
| <th data-key="value">Value</th> | |
| <th data-key="period">Period</th> | |
| <th data-key="change">Change</th> | |
| </tr> | |
| </thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Sectoral Analysis --> | |
| <section id="sectors" class="card" tabindex="0" aria-labelledby="sectorsTitle"> | |
| <h2 id="sectorsTitle" style="margin:0;color:var(--primary)">Sectoral Analysis</h2> | |
| <p class="small muted">Decomposition and performance drivers</p> | |
| <div style="display:grid;grid-template-columns:1fr;gap:12px;margin-top:12px"> | |
| <div class="card" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap"> | |
| <div style="flex:1;min-width:220px"> | |
| <h4 style="margin:0;color:var(--primary)">Primary Growth Drivers</h4> | |
| <ul class="muted small" style="margin-top:8px"> | |
| <li>Services: major contributor</li> | |
| <li>Manufacturing: recovery & development</li> | |
| <li>Export industries: export backbone</li> | |
| <li>Banking: earnings projected +17% in 2025</li> | |
| </ul> | |
| </div> | |
| <div style="width:320px"> | |
| <div id="donutSectors" class="chart" style="height:220px"></div> | |
| <div class="legend" id="donutLegend" style="margin-top:10px"></div> | |
| </div> | |
| </div> | |
| <details class="card" open> | |
| <summary><i class="chev fa fa-chevron-down"></i> Retail & Consumption</summary> | |
| <div style="margin-top:8px" class="muted small"> | |
| Retail sales Q1 2025: 1.708 quadrillion VND (~US$66.83B), +9.9% YoY — strong domestic consumption supports services and retail sectors. | |
| </div> | |
| </details> | |
| </div> | |
| </section> | |
| <!-- FDI & dataset --> | |
| <section id="fdi" class="card" tabindex="0" aria-labelledby="fdiTitle"> | |
| <h2 id="fdiTitle" style="margin:0;color:var(--primary)">FDI & Capital Flows</h2> | |
| <p class="small muted">Monthly breakdown and filters</p> | |
| <div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:12px"> | |
| <label class="muted small">Filter:</label> | |
| <select id="fdifeature" class="btn" style="padding:8px;border-radius:10px"> | |
| <option value="all">Show all</option> | |
| <option value="registered">Registered capital</option> | |
| <option value="disbursed">Disbursed capital</option> | |
| </select> | |
| <div style="margin-left:auto;display:flex;gap:8px"> | |
| <button class="btn" id="copyFDI"><i class="fa fa-clipboard"></i> Copy FDI Table</button> | |
| <button class="btn" id="downloadFDI"><i class="fa fa-download"></i> Download CSV</button> | |
| </div> | |
| </div> | |
| <div style="margin-top:12px;overflow:auto" class="card"> | |
| <table id="fdiTable"> | |
| <thead> | |
| <tr> | |
| <th>Month</th> | |
| <th>Registered (US$B)</th> | |
| <th>Disbursed (US$B)</th> | |
| </tr> | |
| </thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| <div class="card" style="margin-top:12px"> | |
| <h4 style="margin:0;color:var(--primary)">FDI Trend</h4> | |
| <p class="small muted">Registered vs Disbursed (Jan–May 2025)</p> | |
| <div id="fdiChart" class="chart"></div> | |
| </div> | |
| </section> | |
| <!-- Historical Comparison --> | |
| <section id="history" class="card" tabindex="0" aria-labelledby="historyTitle"> | |
| <h2 id="historyTitle" style="margin:0;color:var(--primary)">Historical Comparison</h2> | |
| <p class="small muted">Contextualizing 2025 against recent years</p> | |
| <div style="display:grid;grid-template-columns:1fr;gap:12px;margin-top:12px"> | |
| <div class="card"> | |
| <h4 style="margin:0;color:var(--primary)">Trendline</h4> | |
| <p class="small muted">First-quarter YoY growth 2020–2025</p> | |
| <div id="miniHistory" class="chart" style="height:180px"></div> | |
| </div> | |
| <details class="card"> | |
| <summary><i class="chev fa fa-chevron-down"></i> Year-by-year notes</summary> | |
| <div class="muted small" style="margin-top:8px"> | |
| 2024: 7.1% growth overall. 2025 shows a strong start but may moderate with global headwinds. Long-term fundamentals remain resilient. | |
| </div> | |
| </details> | |
| </div> | |
| </section> | |
| <!-- Outlook & projections --> | |
| <section id="outlook" class="card" tabindex="0" aria-labelledby="outlookTitle"> | |
| <h2 id="outlookTitle" style="margin:0;color:var(--primary)">Economic Outlook & Projections</h2> | |
| <p class="small muted">Scenarios, risks and recommended mitigations</p> | |
| <div style="display:grid;grid-template-columns:1fr;gap:12px;margin-top:12px"> | |
| <div class="card"> | |
| <h4 style="margin:0;color:var(--primary)">Near-term Prospects</h4> | |
| <ul class="muted small" style="margin-top:8px"> | |
| <li>Strong domestic fundamentals support continued growth.</li> | |
| <li>Government target (8.3–8.5%) ambitious vs international forecasts.</li> | |
| <li>Risks centered on trade tensions and geopolitical uncertainty.</li> | |
| </ul> | |
| </div> | |
| <div class="card"> | |
| <h4 style="margin:0;color:var(--primary)">Policy Responses</h4> | |
| <p class="muted small" style="margin-top:8px">Government measures: diversify export markets, strengthen domestic demand, preserve macro stability and use fiscal space for targeted support.</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Risks --> | |
| <section id="risks" class="card" tabindex="0" aria-labelledby="risksTitle"> | |
| <h2 id="risksTitle" style="margin:0;color:var(--primary)">Challenges & Risk Factors</h2> | |
| <p class="small muted">Key risks and suggested mitigations</p> | |
| <div style="display:grid;gap:12px;margin-top:12px"> | |
| <div class="card"> | |
| <h4 style="margin:0;color:var(--primary)">Top Risks</h4> | |
| <ol class="muted small" style="margin-top:8px"> | |
| <li>Global trade tensions affecting exports</li> | |
| <li>US tariff policies pressuring export-oriented firms</li> | |
| <li>Geopolitical instability raising uncertainty</li> | |
| <li>Overreliance on FDI and inflationary pressures</li> | |
| <li>Need to protect macroeconomic stability while pursuing growth</li> | |
| </ol> | |
| </div> | |
| <details class="card"> | |
| <summary><i class="chev fa fa-chevron-down"></i> Mitigation Strategies</summary> | |
| <div class="muted small" style="margin-top:8px"> | |
| Diversify export partners, boost domestic value chains, prudent fiscal policy, and strengthen financial oversight to reduce vulnerabilities. | |
| </div> | |
| </details> | |
| </div> | |
| </section> | |
| <!-- References --> | |
| <section id="references" class="card" tabindex="0" aria-labelledby="referencesTitle"> | |
| <h2 id="referencesTitle" style="margin:0;color:var(--primary)">Sources & References</h2> | |
| <p class="small muted">Cited reports, datasets and links</p> | |
| <ul class="references" style="margin-top:12px"> | |
| <li> | |
| <div class="citation"> | |
| <span class="badge">1</span> | |
| <div style="flex:1"> | |
| Trading Economics - Vietnam GDP Annual Growth Rate — <a href="https://tradingeconomics.com/vietnam/gdp-growth-annual" target="_blank">tradingeconomics.com</a> | |
| </div> | |
| <button class="btn" data-copy="Trading Economics - Vietnam GDP Annual Growth Rate — https://tradingeconomics.com/vietnam/gdp-growth-annual"><i class="fa fa-copy"></i></button> | |
| </div> | |
| </li> | |
| <li> | |
| <div class="citation"> | |
| <span class="badge">2</span> | |
| <div style="flex:1"> | |
| IMF - Vietnam Country Profile — <a href="https://www.imf.org/en/Countries/VNM" target="_blank">imf.org</a> | |
| </div> | |
| <button class="btn" data-copy="IMF - Vietnam Country Profile — https://www.imf.org/en/Countries/VNM"><i class="fa fa-copy"></i></button> | |
| </div> | |
| </li> | |
| <li> | |
| <div class="citation"> | |
| <span class="badge">3</span> | |
| <div style="flex:1"> | |
| World Economics - Vietnam GDP Estimates — <a href="https://www.worldeconomics.com/GDP/Vietnam.gdp" target="_blank">worldeconomics.com</a> | |
| </div> | |
| <button class="btn" data-copy="World Economics - Vietnam GDP Estimates — https://www.worldeconomics.com/GDP/Vietnam.gdp"><i class="fa fa-copy"></i></button> | |
| </div> | |
| </li> | |
| <li style="margin-top:8px"> | |
| <div class="muted small">Full list includes government statistics (GSO), ADB, FocusEconomics, Vietnam Briefing and local news sources. Use citation buttons to copy links quickly.</div> | |
| </li> | |
| </ul> | |
| <div style="display:flex;gap:8px;margin-top:12px"> | |
| <button class="btn" id="copyRefs"><i class="fa fa-clipboard"></i> Copy All References</button> | |
| <button class="btn" id="exportRefsJSON"><i class="fa fa-file-export"></i> Export References (JSON)</button> | |
| </div> | |
| </section> | |
| <footer> | |
| <div>Prepared from public data sources • Interactive report © 2025</div> | |
| </footer> | |
| </main> | |
| </div> | |
| </div> | |
| <div class="tooltip" id="tooltip"></div> | |
| <script> | |
| /* =========================== | |
| Data | |
| ===========================*/ | |
| const data = { | |
| historyQ1: [ | |
| {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:6.93} | |
| ], | |
| forecasts: [ | |
| {name:"World Bank", value:5.8, color:"#06b6d4"}, | |
| {name:"ADB", value:6.6, color:"#0ea5a2"}, | |
| {name:"IMF", value:5.2, color:"#06a5d4"}, | |
| {name:"Government target", value:8.4, color:"#0b7285"} | |
| ], | |
| sectors: [ | |
| {name:"Services", value:45, color:"#06b6d4"}, | |
| {name:"Manufacturing", value:30, color:"#0ea5a2"}, | |
| {name:"Agriculture", value:8, color:"#7dd3fc"}, | |
| {name:"Export-related", value:12, color:"#38bdf8"}, | |
| {name:"Other", value:5, color:"#a7f3d0"} | |
| ], | |
| indicatorsTable:[ | |
| {indicator:"GDP Q1 2025", value:"6.9%", period:"Q1 2025", change:"–"}, | |
| {indicator:"GDP Q2 2025", value:"7.96%", period:"Q2 2025", change:"–"}, | |
| {indicator:"GDP H1 2025", value:"7.52%", period:"H1 2025", change:"Highest H1 since 2011"}, | |
| {indicator:"Inflation May 2025", value:"3.24%", period:"May 2025", change:"–"}, | |
| {indicator:"Inflation Jun 2025", value:"3.57%", period:"Jun 2025", change:"Highest YTD"}, | |
| {indicator:"Unemployment Q1 2025", value:"2.20%", period:"Q1 2025", change:"Down from 2.22%"}, | |
| {indicator:"FDI Registered (Jan–May)", value:"$18.4B", period:"Jan–May 2025", change:"+51% YoY"}, | |
| {indicator:"FDI Disbursed (Jan–May)", value:"$8.9B", period:"Jan–May 2025", change:"–"}, | |
| ], | |
| fdiMonthly:[ | |
| {month:"Jan 2025", registered:5.2, disbursed:2.3}, | |
| {month:"Feb 2025", registered:3.8, disbursed:1.5}, | |
| {month:"Mar 2025", registered:4.1, disbursed:1.8}, | |
| {month:"Apr 2025", registered:2.9, disbursed:1.7}, | |
| {month:"May 2025", registered:2.4, disbursed:1.6} // sums approx to totals | |
| ], | |
| references:[ | |
| {id:1, text:"Trading Economics - Vietnam GDP Annual Growth Rate — https://tradingeconomics.com/vietnam/gdp-growth-annual"}, | |
| {id:2, text:"IMF - Vietnam Country Profile — https://www.imf.org/en/Countries/VNM"}, | |
| {id:3, text:"World Economics - Vietnam GDP Estimates — https://www.worldeconomics.com/GDP/Vietnam.gdp"}, | |
| {id:4, text:"Government of Vietnam - General Statistics Office — https://www.gso.gov.vn/en/"}, | |
| {id:5, text:"Wikipedia - Economy of Vietnam — https://en.wikipedia.org/wiki/Economy_of_Vietnam"}, | |
| {id:6, text:"ADB - Vietnam Country Partnership — https://www.adb.org/countries/viet-nam/main"} | |
| ] | |
| }; | |
| /* =========================== | |
| Utilities | |
| ===========================*/ | |
| const $ = sel => document.querySelector(sel); | |
| const $$ = sel => Array.from(document.querySelectorAll(sel)); | |
| const fmt = n => (typeof n === "number" ? n.toFixed(2) : n); | |
| /* Smooth scrolling for TOC links */ | |
| $$('.toc a').forEach(a => { | |
| a.addEventListener('click', e => { | |
| e.preventDefault(); | |
| const id = a.getAttribute('href').slice(1); | |
| const target = document.getElementById(id); | |
| if(target){ | |
| target.scrollIntoView({behavior:'smooth', block:'start'}); | |
| history.replaceState(null, '', '#'+id); | |
| } | |
| }); | |
| }); | |
| /* =========================== | |
| Theme & LocalStorage | |
| ===========================*/ | |
| const themeToggle = $('#themeToggle'); | |
| const root = document.documentElement; | |
| const savedTheme = localStorage.getItem('vne_theme') || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); | |
| if(savedTheme === 'dark') root.setAttribute('data-theme','dark'); | |
| else root.removeAttribute('data-theme'); | |
| themeToggle.addEventListener('click', () => { | |
| if(root.getAttribute('data-theme') === 'dark'){ | |
| root.removeAttribute('data-theme'); | |
| localStorage.setItem('vne_theme','light'); | |
| themeToggle.innerHTML = '<i class="fa fa-moon"></i>'; | |
| } else { | |
| root.setAttribute('data-theme','dark'); | |
| localStorage.setItem('vne_theme','dark'); | |
| themeToggle.innerHTML = '<i class="fa fa-sun"></i>'; | |
| } | |
| }); | |
| /* =========================== | |
| Populate Indicator Table (sortable) | |
| ===========================*/ | |
| const indicatorTableBody = $('#indicatorTable tbody'); | |
| function renderIndicatorTable(rows){ | |
| indicatorTableBody.innerHTML = rows.map(r => ` | |
| <tr> | |
| <td>${r.indicator}</td> | |
| <td>${r.value}</td> | |
| <td>${r.period}</td> | |
| <td>${r.change}</td> | |
| </tr> | |
| `).join(''); | |
| } | |
| renderIndicatorTable(data.indicatorsTable); | |
| let sortState = {key:null,dir:1}; | |
| $$('#indicatorTable thead th').forEach(th => { | |
| th.addEventListener('click', () => { | |
| const key = th.dataset.key; | |
| if(sortState.key === key) sortState.dir *= -1; | |
| else { sortState.key = key; sortState.dir = 1; } | |
| const sorted = [...data.indicatorsTable].sort((a,b) => { | |
| let va = a[key] || '', vb = b[key] || ''; | |
| // numeric if possible | |
| const na = parseFloat(String(va).replace(/[^0-9.-]+/g,'')), nb = parseFloat(String(vb).replace(/[^0-9.-]+/g,'')); | |
| if(!isNaN(na) && !isNaN(nb)) return (na-nb)*sortState.dir; | |
| return String(va).localeCompare(String(vb)) * sortState.dir; | |
| }); | |
| renderIndicatorTable(sorted); | |
| }); | |
| }); | |
| /* =========================== | |
| Chart Utilities (SVG) | |
| ===========================*/ | |
| function createSVG(w=800,h=300){ | |
| const ns = "http://www.w3.org/2000/svg"; | |
| const svg = document.createElementNS(ns,'svg'); | |
| svg.setAttribute('viewBox',`0 0 ${w} ${h}`); | |
| svg.setAttribute('width','100%'); | |
| svg.setAttribute('height','100%'); | |
| return svg; | |
| } | |
| function clearEl(el){ while(el.firstChild) el.removeChild(el.firstChild); } | |
| /* Tooltip */ | |
| const tooltip = $('#tooltip'); | |
| function showTooltip(html,x,y){ | |
| tooltip.style.display = 'block'; | |
| tooltip.innerHTML = html; | |
| const rect = tooltip.getBoundingClientRect(); | |
| tooltip.style.left = (x)+'px'; | |
| tooltip.style.top = (y)+'px'; | |
| } | |
| function hideTooltip(){ tooltip.style.display = 'none'; } | |
| /* =========================== | |
| Historical Line Chart (2020–2025) | |
| ===========================*/ | |
| function drawHistoryChart(){ | |
| const holder = $('#chartHistory'); | |
| clearEl(holder); | |
| const w = 720, h = 320; | |
| const svg = createSVG(w,h); | |
| holder.appendChild(svg); | |
| const padding = {t:24,r:24,b:36,l:56}; | |
| const innerW = w - padding.l - padding.r; | |
| const innerH = h - padding.t - padding.b; | |
| const values = data.historyQ1.map(d => d.value); | |
| const minV = Math.min(...values) - 1; | |
| const maxV = Math.max(...values) + 1; | |
| // scales | |
| const xs = idx => padding.l + (idx/(values.length-1))*innerW; | |
| const ys = v => padding.t + innerH - ((v - minV)/(maxV - minV))*innerH; | |
| // grid lines and y-axis ticks | |
| const ns = "http://www.w3.org/2000/svg"; | |
| for(let t = Math.ceil(minV); t <= Math.floor(maxV); t+=1){ | |
| const y = ys(t); | |
| const line = document.createElementNS(ns,'line'); | |
| line.setAttribute('x1',padding.l); | |
| line.setAttribute('x2',w-padding.r); | |
| line.setAttribute('y1',y); | |
| line.setAttribute('y2',y); | |
| line.setAttribute('stroke','rgba(0,0,0,0.04)'); | |
| line.setAttribute('stroke-width','1'); | |
| svg.appendChild(line); | |
| const text = document.createElementNS(ns,'text'); | |
| text.setAttribute('x',padding.l-10); | |
| text.setAttribute('y',y+4); | |
| text.setAttribute('text-anchor','end'); | |
| text.setAttribute('fill','var(--muted)'); | |
| text.setAttribute('style','font-size:12px'); | |
| text.textContent = t + '%'; | |
| svg.appendChild(text); | |
| } | |
| // path | |
| let d = ''; | |
| data.historyQ1.forEach((pt,i)=>{ | |
| const x = xs(i), y = ys(pt.value); | |
| d += (i===0?'M':'L') + x + ' ' + y + ' '; | |
| }); | |
| const path = document.createElementNS(ns,'path'); | |
| path.setAttribute('d', d); | |
| path.setAttribute('fill','none'); | |
| path.setAttribute('stroke','#06b6d4'); | |
| path.setAttribute('stroke-width','3'); | |
| path.setAttribute('stroke-linecap','round'); | |
| svg.appendChild(path); | |
| // area fill | |
| const areaD = d + ` L ${padding.l+innerW} ${padding.t+innerH} L ${padding.l} ${padding.t+innerH} Z`; | |
| const area = document.createElementNS(ns,'path'); | |
| area.setAttribute('d', areaD); | |
| area.setAttribute('fill','rgba(6,182,212,0.08)'); | |
| svg.insertBefore(area, path); | |
| // points and hover | |
| data.historyQ1.forEach((pt,i)=>{ | |
| const x = xs(i), y = ys(pt.value); | |
| const circle = document.createElementNS(ns,'circle'); | |
| circle.setAttribute('cx',x); | |
| circle.setAttribute('cy',y); | |
| circle.setAttribute('r',5); | |
| circle.setAttribute('fill','#fff'); | |
| circle.setAttribute('stroke','#06b6d4'); | |
| circle.setAttribute('stroke-width',2); | |
| circle.style.cursor='pointer'; | |
| svg.appendChild(circle); | |
| circle.addEventListener('mouseenter', (e) => { | |
| const html = `<strong>${pt.year}</strong><div class="muted small">${pt.value}% YoY</div>`; | |
| const rect = holder.getBoundingClientRect(); | |
| showTooltip(html, rect.left + x, rect.top + y - 8); | |
| }); | |
| circle.addEventListener('mouseleave', hideTooltip); | |
| }); | |
| // labels (years) | |
| data.historyQ1.forEach((pt,i)=>{ | |
| const x = xs(i), y = padding.t + innerH + 18; | |
| const nsTxt = document.createElementNS("http://www.w3.org/2000/svg",'text'); | |
| nsTxt.setAttribute('x',x); | |
| nsTxt.setAttribute('y',y); | |
| nsTxt.setAttribute('text-anchor','middle'); | |
| nsTxt.setAttribute('fill','var(--muted)'); | |
| nsTxt.setAttribute('style','font-size:12px'); | |
| nsTxt.textContent = pt.year; | |
| svg.appendChild(nsTxt); | |
| }); | |
| } | |
| drawHistoryChart(); | |
| /* =========================== | |
| Forecast Bar Chart | |
| ===========================*/ | |
| function drawForecastChart(){ | |
| const holder = $('#chartForecast'); | |
| clearEl(holder); | |
| const w=420,h=260; | |
| const svg = createSVG(w,h); | |
| holder.appendChild(svg); | |
| const ns = "http://www.w3.org/2000/svg"; | |
| const padding = {t:20,r:20,b:40,l:50}; | |
| const innerW = w - padding.l - padding.r; | |
| const innerH = h - padding.t - padding.b; | |
| const maxVal = Math.max(...data.forecasts.map(f=>f.value))+1; | |
| data.forecasts.forEach((f,i)=>{ | |
| const barW = innerW / data.forecasts.length * 0.7; | |
| const gap = innerW / data.forecasts.length; | |
| const x = padding.l + i*gap + (gap - barW)/2; | |
| const y = padding.t + innerH - (f.value/maxVal)*innerH; | |
| const rect = document.createElementNS(ns,'rect'); | |
| rect.setAttribute('x', x); | |
| rect.setAttribute('y', y); | |
| rect.setAttribute('width', barW); | |
| rect.setAttribute('height', padding.t + innerH - y); | |
| rect.setAttribute('rx',8); | |
| rect.setAttribute('fill', f.color); | |
| svg.appendChild(rect); | |
| rect.addEventListener('mouseenter', (ev)=>{ | |
| const rectEl = holder.getBoundingClientRect(); | |
| showTooltip(`<strong>${f.name}</strong><div class="muted small">${f.value}%</div>`, rectEl.left + x + barW/2, rectEl.top + y); | |
| }); | |
| rect.addEventListener('mouseleave', hideTooltip); | |
| // labels | |
| const text = document.createElementNS(ns,'text'); | |
| text.setAttribute('x', x + barW/2); | |
| text.setAttribute('y', padding.t + innerH + 18); | |
| text.setAttribute('text-anchor','middle'); | |
| text.setAttribute('fill','var(--muted)'); | |
| text.setAttribute('style','font-size:12px'); | |
| text.textContent = f.name.replace('Government target','Gov target'); | |
| svg.appendChild(text); | |
| }); | |
| // y-axis ticks | |
| for(let t=0;t<=Math.ceil(maxVal);t+=1){ | |
| const y = padding.t + innerH - (t/maxVal)*innerH; | |
| const tx = document.createElementNS(ns,'text'); | |
| tx.setAttribute('x',padding.l-8); | |
| tx.setAttribute('y',y+4); | |
| tx.setAttribute('text-anchor','end'); | |
| tx.setAttribute('fill','var(--muted)'); | |
| tx.setAttribute('style','font-size:11px'); | |
| tx.textContent = t + '%'; | |
| svg.appendChild(tx); | |
| } | |
| // legend | |
| const legend = $('#forecastLegend'); | |
| legend.innerHTML = ''; | |
| data.forecasts.forEach(f => { | |
| const item = document.createElement('div'); | |
| item.className='legend-item'; | |
| item.innerHTML = `<span class="swatch" style="background:${f.color}"></span><div class="small muted">${f.name} — ${f.value}%</div>`; | |
| legend.appendChild(item); | |
| }); | |
| } | |
| drawForecastChart(); | |
| /* =========================== | |
| Donut Chart for Sectors | |
| ===========================*/ | |
| function drawDonut(){ | |
| const holder = $('#donutSectors'); | |
| clearEl(holder); | |
| const w=320,h=220; | |
| const svg = createSVG(w,h); | |
| holder.appendChild(svg); | |
| const ns = "http://www.w3.org/2000/svg"; | |
| const cx = w/2, cy = h/2 - 10, r = Math.min(w,h)/3; | |
| const total = data.sectors.reduce((s,d)=>s+d.value,0); | |
| let angle = -Math.PI/2; | |
| const donutGroup = document.createElementNS(ns,'g'); | |
| svg.appendChild(donutGroup); | |
| data.sectors.forEach((s,idx)=>{ | |
| const sliceAngle = (s.value/total)*(Math.PI*2); | |
| const end = angle + sliceAngle; | |
| const x1 = cx + r*Math.cos(angle); | |
| const y1 = cy + r*Math.sin(angle); | |
| const x2 = cx + r*Math.cos(end); | |
| const y2 = cy + r*Math.sin(end); | |
| const large = sliceAngle > Math.PI ? 1 : 0; | |
| const pathD = `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z`; | |
| const path = document.createElementNS(ns,'path'); | |
| path.setAttribute('d', pathD); | |
| path.setAttribute('fill', s.color); | |
| path.setAttribute('opacity',0.95); | |
| donutGroup.appendChild(path); | |
| path.addEventListener('mouseenter', () => { | |
| const rect = holder.getBoundingClientRect(); | |
| showTooltip(`<strong>${s.name}</strong><div class="muted small">${s.value}%</div>`, rect.left + cx + (r*Math.cos(angle+sliceAngle/2)), rect.top + cy + (r*Math.sin(angle+sliceAngle/2))); | |
| }); | |
| path.addEventListener('mouseleave', hideTooltip); | |
| angle = end; | |
| }); | |
| // inner circle to make donut | |
| const inner = document.createElementNS(ns,'circle'); | |
| inner.setAttribute('cx',cx); | |
| inner.setAttribute('cy',cy); | |
| inner.setAttribute('r',r*0.6); | |
| inner.setAttribute('fill','var(--card)'); | |
| inner.setAttribute('stroke','rgba(0,0,0,0.02)'); | |
| svg.appendChild(inner); | |
| // legend mapping with toggles | |
| const legend = $('#donutLegend'); | |
| legend.innerHTML = ''; | |
| data.sectors.forEach(s=>{ | |
| const item = document.createElement('div'); | |
| item.className = 'legend-item'; | |
| item.innerHTML = `<span class="swatch" style="background:${s.color}"></span><div class="small muted">${s.name} — ${s.value}%</div>`; | |
| legend.appendChild(item); | |
| }); | |
| } | |
| drawDonut(); | |
| /* =========================== | |
| FDI Table and Chart | |
| ===========================*/ | |
| function renderFDITable(filter='all'){ | |
| const tbody = $('#fdiTable tbody'); | |
| const rows = data.fdiMonthly.map(r => { | |
| return `<tr> | |
| <td>${r.month}</td> | |
| <td>${r.registered.toFixed(2)}</td> | |
| <td>${r.disbursed.toFixed(2)}</td> | |
| </tr>`; | |
| }).join(''); | |
| tbody.innerHTML = rows; | |
| } | |
| renderFDITable(); | |
| function drawFDIChart(){ | |
| const holder = $('#fdiChart'); | |
| clearEl(holder); | |
| const w=640,h=260; | |
| const svg = createSVG(w,h); | |
| holder.appendChild(svg); | |
| const ns = "http://www.w3.org/2000/svg"; | |
| const padding = {t:20,r:20,b:50,l:50}; | |
| const innerW = w - padding.l - padding.r; | |
| const innerH = h - padding.t - padding.b; | |
| const maxV = Math.max(...data.fdiMonthly.flatMap(x=>[x.registered,x.disbursed]))+1; | |
| // stacked bars side-by-side | |
| data.fdiMonthly.forEach((m,i)=>{ | |
| const gap = innerW / data.fdiMonthly.length; | |
| const barW = gap*0.35; | |
| const x = padding.l + i*gap; | |
| const yReg = padding.t + innerH - (m.registered/maxV)*innerH; | |
| const yDis = padding.t + innerH - (m.disbursed/maxV)*innerH; | |
| const rectR = document.createElementNS(ns,'rect'); | |
| rectR.setAttribute('x', x - barW/2); | |
| rectR.setAttribute('y', yReg); | |
| rectR.setAttribute('width', barW); | |
| rectR.setAttribute('height', padding.t + innerH - yReg); | |
| rectR.setAttribute('fill','#06b6d4'); | |
| rectR.setAttribute('rx',6); | |
| svg.appendChild(rectR); | |
| const rectD = document.createElementNS(ns,'rect'); | |
| rectD.setAttribute('x', x + barW/2); | |
| rectD.setAttribute('y', yDis); | |
| rectD.setAttribute('width', barW); | |
| rectD.setAttribute('height', padding.t + innerH - yDis); | |
| rectD.setAttribute('fill','#0ea5a2'); | |
| rectD.setAttribute('rx',6); | |
| svg.appendChild(rectD); | |
| [rectR,rectD].forEach((el,kindIdx)=>{ | |
| el.addEventListener('mouseenter',()=>{ | |
| const rect = holder.getBoundingClientRect(); | |
| const text = kindIdx===0 ? `Registered: ${m.registered}B` : `Disbursed: ${m.disbursed}B`; | |
| showTooltip(`<strong>${m.month}</strong><div class="muted small">${text}</div>`, rect.left + x, rect.top + (kindIdx===0?yReg:yDis)); | |
| }); | |
| el.addEventListener('mouseleave', hideTooltip); | |
| }); | |
| // label | |
| const label = document.createElementNS(ns,'text'); | |
| label.setAttribute('x', x); | |
| label.setAttribute('y', padding.t + innerH + 22); | |
| label.setAttribute('text-anchor','middle'); | |
| label.setAttribute('fill','var(--muted)'); | |
| label.setAttribute('style','font-size:11px'); | |
| label.textContent = m.month.split(' ')[0]; | |
| svg.appendChild(label); | |
| }); | |
| // legend | |
| const legendDiv = document.createElement('div'); | |
| legendDiv.style.marginTop = '8px'; | |
| } | |
| drawFDIChart(); | |
| /* =========================== | |
| Interactions: search, copy summary, export | |
| ===========================*/ | |
| const globalSearch = $('#globalSearch'); | |
| globalSearch.addEventListener('input', (e) => { | |
| const q = e.target.value.trim().toLowerCase(); | |
| // highlight and open matching sections, show first match | |
| const sections = Array.from(document.querySelectorAll('main.content section')); | |
| let firstMatch = null; | |
| sections.forEach(sec => { | |
| const txt = sec.textContent.toLowerCase(); | |
| if(q && txt.includes(q)){ | |
| sec.classList.add('highlight'); | |
| sec.style.boxShadow = '0 6px 30px rgba(6,182,212,0.06)'; | |
| if(!firstMatch) firstMatch = sec; | |
| } else { | |
| sec.classList.remove('highlight'); | |
| sec.style.boxShadow = ''; | |
| } | |
| }); | |
| if(firstMatch){ | |
| firstMatch.scrollIntoView({behavior:'smooth', block:'center'}); | |
| } | |
| }); | |
| /* Copy executive summary to clipboard */ | |
| $('#copySummary').addEventListener('click', async () => { | |
| const summary = document.querySelector('#executive .summary p').textContent.trim(); | |
| try{ | |
| await navigator.clipboard.writeText(summary); | |
| alert('Executive summary copied to clipboard.'); | |
| }catch(e){ | |
| console.warn(e); | |
| alert('Copy failed — permission denied.'); | |
| } | |
| }); | |
| /* Export indicators to CSV */ | |
| $('#exportCSV').addEventListener('click', () => { | |
| const rows = data.indicatorsTable; | |
| const csv = ['Indicator,Value,Period,Change', ...rows.map(r=>`"${r.indicator}","${r.value}","${r.period}","${r.change}"`)].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_indicators_2025.csv'; | |
| document.body.appendChild(a); a.click(); a.remove(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| /* Export JSON */ | |
| $('#exportJSON').addEventListener('click', () => { | |
| const payload = { | |
| meta:{title:'Vietnam Economic Growth Report 2025', date:2025}, | |
| data | |
| }; | |
| const blob = new Blob([JSON.stringify(payload,null,2)],{type:'application/json'}); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; a.download = 'vietnam_report_2025.json'; | |
| document.body.appendChild(a); a.click(); a.remove(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| /* Print */ | |
| $('#printBtn').addEventListener('click', () => window.print()); | |
| /* Copy FDI table */ | |
| $('#copyFDI').addEventListener('click', async () => { | |
| const txt = data.fdiMonthly.map(r=>`${r.month}: Registered ${r.registered}B — Disbursed ${r.disbursed}B`).join('\\n'); | |
| try{ await navigator.clipboard.writeText(txt); alert('FDI table copied to clipboard.'); }catch(e){ alert('Copy failed.'); } | |
| }); | |
| /* Download FDI CSV */ | |
| $('#downloadFDI').addEventListener('click', () => { | |
| const csv = ['Month,Registered,Disbursed', ...data.fdiMonthly.map(r=>`${r.month},${r.registered},${r.disbursed}`)].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 = 'fdi_jan_may_2025.csv'; | |
| document.body.appendChild(a); a.click(); a.remove(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| /* Copy references buttons */ | |
| $$('.references button').forEach(btn=>{ | |
| btn.addEventListener('click', async () => { | |
| const txt = btn.dataset.copy; | |
| try{ await navigator.clipboard.writeText(txt); alert('Citation copied.'); }catch(e){ alert('Copy failed.'); } | |
| }); | |
| }); | |
| $('#copyRefs').addEventListener('click', async () => { | |
| const all = data.references.map(r=>r.text).join('\\n'); | |
| try{ await navigator.clipboard.writeText(all); alert('All references copied.'); }catch(e){ alert('Copy failed.'); } | |
| }); | |
| $('#exportRefsJSON').addEventListener('click', () => { | |
| const blob = new Blob([JSON.stringify(data.references,null,2)],{type:'application/json'}); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; a.download = 'references.json'; | |
| document.body.appendChild(a); a.click(); a.remove(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| /* Export entire visible report to JSON (quick) - reuse exportJSON */ | |
| $('#readMoreExec').addEventListener('click', () => { | |
| const more = `Vietnam's economy maintained robust growth in H1 2025 with leads from services and manufacturing. Macroeconomic control (inflation ~3.5%) and strong FDI flows underpin resilience, yet external trade tensions pose key risks.`; | |
| alert(more); | |
| }); | |
| /* =========================== | |
| Table filtering (FDI feature) | |
| ===========================*/ | |
| $('#fdifeature').addEventListener('change', (e)=>{ | |
| const v = e.target.value; | |
| const tbody = document.querySelectorAll('#fdiTable tbody tr'); | |
| tbody.forEach(tr => tr.style.display = 'table-row'); // single view in table; filtering shows/hides columns | |
| const cells = $('#fdiTable thead').querySelectorAll('th'); | |
| if(v === 'registered'){ | |
| cells[1].style.display='table-cell'; | |
| cells[2].style.display='none'; | |
| $('#fdiTable tbody tr').forEach(tr=> tr.children[1].style.display='table-cell', tr.children[2].style.display='none'); | |
| } else if(v === 'disbursed'){ | |
| cells[1].style.display='none'; | |
| cells[2].style.display='table-cell'; | |
| $('#fdiTable tbody tr').forEach(tr=> tr.children[1].style.display='none', tr.children[2].style.display='table-cell'); | |
| } else { | |
| cells[1].style.display='table-cell'; | |
| cells[2].style.display='table-cell'; | |
| $('#fdiTable tbody tr').forEach(tr=> tr.children[1].style.display='table-cell', tr.children[2].style.display='table-cell'); | |
| } | |
| }); | |
| /* =========================== | |
| TOC Active Link and Progress (IntersectionObserver) | |
| ===========================*/ | |
| const sections = Array.from(document.querySelectorAll('main.content section')); | |
| const tocLinks = Array.from(document.querySelectorAll('.toc a')); | |
| const progressEl = $('#tocProgress'); | |
| const obs = new IntersectionObserver(entries=>{ | |
| entries.forEach(entry=>{ | |
| const id = entry.target.id; | |
| const link = document.querySelector('.toc a[data-id="'+id+'"]'); | |
| if(entry.isIntersecting){ | |
| tocLinks.forEach(l=>l.classList.remove('active')); | |
| if(link) link.classList.add('active'); | |
| localStorage.setItem('vne_last_section', id); | |
| } | |
| // calculate progress: visible proportion of document | |
| const visible = Math.max(0,entry.intersectionRatio); | |
| // approximate progress based on scroll position | |
| const docHeight = document.body.scrollHeight - window.innerHeight; | |
| const scrolled = window.scrollY; | |
| const progressPerc = Math.min(100, Math.round((scrolled/docHeight)*100)); | |
| progressEl.style.width = progressPerc + '%'; | |
| progressEl.style.display = 'block'; | |
| // animate width using transform | |
| progressEl.style.transform = `translateX(${0}%)`; | |
| $('#tocProgress').style.width = progressPerc + '%'; | |
| }); | |
| }, {threshold:[0.15,0.45,0.75]}); | |
| sections.forEach(s => obs.observe(s)); | |
| /* Save & go bookmark (stores last open section id) */ | |
| $('#saveBookmark').addEventListener('click', () => { | |
| const active = document.querySelector('.toc a.active'); | |
| if(active){ | |
| const id = active.dataset.id; | |
| localStorage.setItem('vne_bookmark', id); | |
| alert('Saved current section: '+id); | |
| } else alert('No active section selected.'); | |
| }); | |
| $('#gotoBookmark').addEventListener('click', () => { | |
| const id = localStorage.getItem('vne_bookmark'); | |
| if(id){ | |
| const target = document.getElementById(id); | |
| target.scrollIntoView({behavior:'smooth', block:'start'}); | |
| } else alert('No bookmark saved.'); | |
| }); | |
| /* Restore last section if any */ | |
| window.addEventListener('load', () => { | |
| const last = localStorage.getItem('vne_last_section'); | |
| if(last){ | |
| const link = document.querySelector('.toc a[data-id="'+last+'"]'); | |
| if(link) link.classList.add('active'); | |
| } | |
| }); | |
| /* =========================== | |
| Small mini history chart | |
| ===========================*/ | |
| function drawMiniHistory(){ | |
| const holder = $('#miniHistory'); | |
| clearEl(holder); | |
| const w=560,h=160; | |
| const svg = createSVG(w,h); | |
| holder.appendChild(svg); | |
| const padding={t:16,b:30,l:40,r:16}; | |
| const innerW = w-padding.l-padding.r; | |
| const innerH = h-padding.t-padding.b; | |
| const values = data.historyQ1.map(d=>d.value); | |
| const minV = Math.min(...values)-1; | |
| const maxV = Math.max(...values)+1; | |
| const xs = idx => padding.l + (idx/(values.length-1))*innerW; | |
| const ys = v => padding.t + innerH - ((v-minV)/(maxV-minV))*innerH; | |
| const ns = "http://www.w3.org/2000/svg"; | |
| let d = ''; | |
| data.historyQ1.forEach((pt,i)=>{ | |
| d += (i===0?'M':'L') + xs(i) + ' ' + ys(pt.value) + ' '; | |
| }); | |
| const path = document.createElementNS(ns,'path'); | |
| path.setAttribute('d',d); | |
| path.setAttribute('fill','none'); | |
| path.setAttribute('stroke','#0ea5a2'); | |
| path.setAttribute('stroke-width','2'); | |
| path.setAttribute('stroke-linecap','round'); | |
| svg.appendChild(path); | |
| data.historyQ1.forEach((pt,i)=>{ | |
| const c = document.createElementNS(ns,'circle'); | |
| c.setAttribute('cx',xs(i)); | |
| c.setAttribute('cy',ys(pt.value)); | |
| c.setAttribute('r',3); | |
| c.setAttribute('fill','#fff'); | |
| c.setAttribute('stroke','#0ea5a2'); | |
| c.setAttribute('stroke-width',1.5); | |
| svg.appendChild(c); | |
| }); | |
| } | |
| drawMiniHistory(); | |
| /* =========================== | |
| Make FDI table rows clickable to copy | |
| ===========================*/ | |
| $('#fdiTable tbody').addEventListener('click', async (e) => { | |
| let tr = e.target.closest('tr'); | |
| if(!tr) return; | |
| const cells = Array.from(tr.children).map(td=>td.textContent.trim()).join(' | '); | |
| try{ await navigator.clipboard.writeText(cells); alert('Row copied: '+cells); }catch(e){ alert('Copy failed'); } | |
| }); | |
| /* Accessibility: keyboard navigation for TOC */ | |
| $$('.toc a').forEach((a,i)=>{ | |
| a.setAttribute('tabindex',0); | |
| a.addEventListener('keydown', (e)=>{ | |
| if(e.key === 'Enter') a.click(); | |
| }); | |
| }); | |
| /* Resize handlers: redraw charts on resize to keep resolution appropriate */ | |
| let resizeTimer; | |
| window.addEventListener('resize', () => { | |
| clearTimeout(resizeTimer); | |
| resizeTimer = setTimeout(()=>{ drawHistoryChart(); drawForecastChart(); drawDonut(); drawFDIChart(); drawMiniHistory(); }, 200); | |
| }); | |
| /* Initialize other UI states */ | |
| (function init(){ | |
| renderFDITable(); | |
| // populate toc active | |
| const hash = location.hash.replace('#',''); | |
| if(hash){ | |
| const target = document.getElementById(hash); | |
| if(target) target.scrollIntoView(); | |
| } | |
| })(); | |
| </script> | |
| </body> | |
| </html> |