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