/* ════════════════════════════════════════════════════════ * page-growth.js * growth_analysis.html 的 Chart.js 圖表 * Chart.js 由 analysis-chart-theme.js 懶載入,避免首屏阻塞。 * ════════════════════════════════════════════════════════ */ (function () { 'use strict'; let chartsRendered = false; function readGrowthData() { const node = document.getElementById('chart-data'); if (!node) return null; try { return JSON.parse(node.textContent || '{}'); } catch (error) { console.error('[growth_analysis] chart data parse failed:', error); return null; } } const data = readGrowthData(); if (!data) return; const rootStyle = getComputedStyle(document.documentElement); const token = (name, fallback) => rootStyle.getPropertyValue(name).trim() || fallback; const chartPalette = { caramel: token('--momo-page-chart-2', '#c96442'), caramelSoft: token('--momo-warm-caramel-soft', 'rgba(201, 100, 66, 0.58)'), honey: token('--momo-page-accent', '#c89043'), honeySoft: token('--momo-page-accent-soft', 'rgba(200, 144, 67, 0.14)'), rust: token('--momo-danger-text', '#7a3210'), rustSoft: token('--momo-danger-bg', '#efd3c4') }; const chartInstances = []; function rememberChart(chart) { if (chart) chartInstances.push(chart); return chart; } function stabilizeCharts() { window.requestAnimationFrame(() => { chartInstances.forEach(chart => { if (!chart) return; if (typeof chart.resize === 'function') chart.resize(); if (typeof chart.update === 'function') chart.update('none'); }); window.dispatchEvent(new Event('resize')); }); } function formatShort(value, mode) { const n = Number(value || 0); if (mode === 'pct') return `${n.toFixed(1)}%`; if (mode === 'currency') return `$${Math.round(n).toLocaleString()}`; return Math.round(n).toLocaleString(); } function renderHtmlBars(canvasId, labels, values, options = {}) { const canvas = document.getElementById(canvasId); const wrap = canvas ? canvas.closest('.ga-chart-card__body') : null; if (!wrap || wrap.querySelector('.ga-chart-fallback')) return; const pairs = (labels || []).map((label, index) => ({ label: String(label || ''), value: Number((values || [])[index] || 0) })).filter(item => Number.isFinite(item.value)); if (!pairs.length) return; const max = Math.max(...pairs.map(item => Math.abs(item.value)), 1); wrap.classList.add('has-html-chart'); canvas.setAttribute('aria-hidden', 'true'); const chart = document.createElement('div'); chart.className = 'ga-chart-fallback'; pairs.forEach(item => { const bar = document.createElement('div'); bar.className = `ga-chart-fallback__bar ${item.value < 0 ? 'is-negative' : ''}`; bar.style.setProperty('--bar-h', `${Math.max(4, Math.round(Math.abs(item.value) / max * 100))}%`); const label = document.createElement('span'); label.textContent = item.label; const value = document.createElement('strong'); value.textContent = formatShort(item.value, options.mode); bar.append(label, value); chart.appendChild(bar); }); wrap.appendChild(chart); } function renderHtmlChartFallbacks() { renderHtmlBars('revenueChart', data.labels, data.revenue, { mode: 'currency' }); renderHtmlBars('momChart', data.labels, data.mom, { mode: 'pct' }); renderHtmlBars('aovChart', data.labels, data.aov, { mode: 'currency' }); renderHtmlBars('marginChart', data.labels, data.margin_rate, { mode: 'pct' }); } function loadChartJs() { if (window.EwoooCChartTheme && window.EwoooCChartTheme.loadChartJs) { return window.EwoooCChartTheme.loadChartJs(); } if (window.Chart) return Promise.resolve(window.Chart); return Promise.reject(new Error('Chart.js loader unavailable')); } function renderCharts() { if (chartsRendered) return; chartsRendered = true; Chart.defaults.color = token('--momo-text-secondary', '#6b6155'); Chart.defaults.borderColor = token('--momo-border-light', 'rgba(42, 37, 32, 0.10)'); Chart.defaults.font.family = token('--momo-font-family', "'Inter', system-ui, sans-serif"); const revenueEl = document.getElementById('revenueChart'); const momEl = document.getElementById('momChart'); const aovEl = document.getElementById('aovChart'); const marginEl = document.getElementById('marginChart'); if (!revenueEl || !momEl || !aovEl || !marginEl) return; rememberChart(new Chart(revenueEl, { type: 'bar', data: { labels: data.labels, datasets: [ { label: '月營收 ($)', data: data.revenue, backgroundColor: chartPalette.caramelSoft, order: 2 }, { label: 'YoY 年增率 (%)', data: data.yoy, type: 'line', borderColor: chartPalette.rust, borderWidth: 2, yAxisID: 'y1', order: 1, tension: 0.3 } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, title: { display: true, text: '金額 ($)' } }, y1: { position: 'right', grid: { drawOnChartArea: false }, title: { display: true, text: '成長率 (%)' } } } } })); rememberChart(new Chart(momEl, { type: 'bar', data: { labels: data.labels, datasets: [{ label: 'MoM 月增率 (%)', data: data.mom, backgroundColor: ctx => ctx.raw >= 0 ? chartPalette.honeySoft : chartPalette.rustSoft }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } })); rememberChart(new Chart(aovEl, { type: 'line', data: { labels: data.labels, datasets: [{ label: '平均單價 ($)', data: data.aov, borderColor: chartPalette.caramel, backgroundColor: chartPalette.caramelSoft, fill: true, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } })); rememberChart(new Chart(marginEl, { type: 'line', data: { labels: data.labels, datasets: [{ label: '毛利率 (%)', data: data.margin_rate, borderColor: chartPalette.honey, backgroundColor: chartPalette.honeySoft, fill: true, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } })); stabilizeCharts(); renderHtmlChartFallbacks(); } function bootCharts() { document.documentElement.dataset.growthCharts = 'loading'; loadChartJs() .then(() => { renderCharts(); document.documentElement.dataset.growthCharts = 'ready'; }) .catch(error => { document.documentElement.dataset.growthCharts = 'error'; document.documentElement.dataset.growthChartsError = error && error.message ? error.message : String(error); console.error('[growth_analysis] Chart.js 載入失敗:', error); }); } function scheduleChartBoot() { window.setTimeout(bootCharts, 0); } function observeCharts() { const targets = Array.from(document.querySelectorAll('.ga-chart-card')); if (!targets.length) return; if (!('IntersectionObserver' in window)) { bootCharts(); return; } const observer = new IntersectionObserver(entries => { if (!entries.some(entry => entry.isIntersecting)) return; observer.disconnect(); bootCharts(); }, { rootMargin: '220px 0px' }); targets.forEach(target => observer.observe(target)); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', scheduleChartBoot, { once: true }); } else { scheduleChartBoot(); } })();