// ==================== Baby Tracker Charts ==================== window.BabyCharts = (() => { 'use strict'; const _charts = new Map(); // domId → { chart, handler } let _sharedResizeHandler = null; let _resizeTimer = null; let _echartsReady = null; /** Lazy-load ECharts from CDN — only when growth tab is first opened */ function ensureECharts() { if (_echartsReady) return _echartsReady; if (typeof echarts !== 'undefined') { _echartsReady = Promise.resolve(true); return _echartsReady; } _echartsReady = new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js'; script.onload = () => resolve(true); script.onerror = () => reject(new Error('ECharts 加载失败,请检查网络')); document.head.appendChild(script); }); return _echartsReady; } function ensureSharedResize() { if (_sharedResizeHandler) return; _sharedResizeHandler = () => { clearTimeout(_resizeTimer); _resizeTimer = setTimeout(() => { for (const { chart } of _charts.values()) { try { chart.resize(); } catch { /* disposed */ } } }, 150); }; window.addEventListener('resize', _sharedResizeHandler); } function removeSharedResizeIfEmpty() { if (_charts.size === 0 && _sharedResizeHandler) { window.removeEventListener('resize', _sharedResizeHandler); _sharedResizeHandler = null; clearTimeout(_resizeTimer); _resizeTimer = null; } } async function renderChart(domId, title, userPoints, standardKey, yRange) { const el = document.getElementById(domId); if (!el) return; await ensureECharts(); if (typeof echarts === 'undefined') return; disposeByDomId(domId); const chart = echarts.init(el, null, { renderer: 'canvas' }); // Read colors from CSS custom properties so dark mode picks up automatically const style = getComputedStyle(document.documentElement); const babyColor = style.getPropertyValue('--accent').trim() || '#D4896A'; const inkMuted = style.getPropertyValue('--muted').trim() || '#8A8580'; const gridLine = style.getPropertyValue('--line-soft').trim() || '#EFEAE3'; const inkColor = style.getPropertyValue('--ink').trim() || '#2C2A28'; const surfaceColor = style.getPropertyValue('--surface').trim() || '#FFFFFF'; const option = { backgroundColor: 'transparent', title: { text: title, left: '8px', top: '8px', textStyle: { fontSize: 13, fontWeight: 650, color: inkColor } }, grid: { top: 46, right: 16, bottom: 28, left: 36, containLabel: false }, tooltip: { trigger: 'axis', confine: true, backgroundColor: inkColor, borderWidth: 0, textStyle: { color: surfaceColor, fontSize: 12 }, formatter(params) { const userItem = params.find(p => p.seriesName === '宝宝数据'); if (!userItem || !userItem.value) return ''; const [age, val] = userItem.value; return `月龄 ${Number(age).toFixed(1)} 月
${val}`; } }, xAxis: { type: 'value', min: 0, max: 24, interval: 3, axisLine: { show: false }, axisTick: { show: false }, splitLine: { show: false }, axisLabel: { formatter: '{value}月', color: inkMuted, fontSize: 11 } }, yAxis: { type: 'value', min: yRange ? (value) => Math.floor(Math.min(value.min, yRange[0]) * 0.95) : undefined, scale: true, axisLine: { show: false }, axisTick: { show: false }, splitLine: { lineStyle: { type: 'dashed', color: gridLine } }, axisLabel: { color: inkMuted, fontSize: 11 } }, series: [ ...BabyUtils.generateStandardSeries(standardKey), { name: '宝宝数据', type: 'line', data: userPoints, smooth: false, symbol: 'circle', symbolSize: 7, itemStyle: { color: babyColor, borderColor: surfaceColor, borderWidth: 2 }, lineStyle: { color: babyColor, width: 2.5 }, z: 10, markPoint: userPoints.length ? { data: [{ coord: userPoints[userPoints.length - 1], symbolSize: 1, label: { show: true, position: 'top', formatter: () => String(userPoints[userPoints.length - 1][1]), color: babyColor, fontWeight: 700, fontSize: 12 } }] } : undefined } ] }; chart.setOption(option); ensureSharedResize(); _charts.set(domId, { chart, handler: null }); } /** Helper: read CSS variable with fallback */ function _cssVar(name, fallback) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback; } /** Render a bar chart (used by report tab) */ async function renderBarChart(domId, title, labels, values) { const el = document.getElementById(domId); if (!el) return; await ensureECharts(); if (typeof echarts === 'undefined') return; disposeByDomId(domId); const accentColor = _cssVar('--accent', '#D4896A'); const inkColor = _cssVar('--ink', '#2C2A28'); const mutedColor = _cssVar('--muted', '#8A8580'); const surfaceColor = _cssVar('--surface', '#FFFFFF'); const lineColor = _cssVar('--line-soft', '#F0EBE3'); const chart = echarts.init(el, null, { renderer: 'canvas' }); chart.setOption({ backgroundColor: 'transparent', title: { text: title, left: '8px', top: '8px', textStyle: { fontSize: 13, fontWeight: 650, color: inkColor } }, grid: { top: 44, right: 16, bottom: 32, left: 44, containLabel: false }, tooltip: { trigger: 'axis', confine: true, backgroundColor: inkColor, borderWidth: 0, textStyle: { color: surfaceColor, fontSize: 12 } }, xAxis: { type: 'category', data: labels, axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: mutedColor, fontSize: 10, rotate: labels.length > 10 ? 45 : 0 } }, yAxis: { type: 'value', axisLine: { show: false }, axisTick: { show: false }, splitLine: { lineStyle: { type: 'dashed', color: lineColor } }, axisLabel: { color: mutedColor, fontSize: 11 } }, series: [{ type: 'bar', data: values, itemStyle: { color: accentColor, borderRadius: [8, 8, 0, 0] }, barMaxWidth: 40 }] }); ensureSharedResize(); _charts.set(domId, { chart, handler: null }); } /** Render a donut/pie chart (used by report tab) */ async function renderDonutChart(domId, title, data) { const el = document.getElementById(domId); if (!el) return; await ensureECharts(); if (typeof echarts === 'undefined') return; disposeByDomId(domId); const inkColor = _cssVar('--ink', '#2C2A28'); const surfaceColor = _cssVar('--surface', '#FFFFFF'); const mutedColor = _cssVar('--muted', '#8A8580'); const palette = [ _cssVar('--milk', '#6B9AC4'), _cssVar('--accent', '#D4896A'), _cssVar('--food', '#D4A574'), _cssVar('--meds', '#9B8EC4'), _cssVar('--poo', '#7D9B76') ]; const chart = echarts.init(el, null, { renderer: 'canvas' }); chart.setOption({ backgroundColor: 'transparent', title: { text: title, left: '8px', top: '8px', textStyle: { fontSize: 13, fontWeight: 650, color: inkColor } }, tooltip: { trigger: 'item', confine: true, backgroundColor: inkColor, borderWidth: 0, textStyle: { color: surfaceColor, fontSize: 12 }, formatter: (p) => `${p.name}: ${p.value}ml (${p.percent}%)` }, legend: { bottom: 8, textStyle: { color: mutedColor, fontSize: 11 }, itemWidth: 10, itemHeight: 10, itemGap: 16 }, series: [{ type: 'pie', radius: ['55%', '78%'], center: ['50%', '46%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 6, borderColor: surfaceColor, borderWidth: 2 }, label: { show: true, position: 'outside', formatter: '{b}\n{d}%', color: mutedColor, fontSize: 11 }, emphasis: { label: { fontSize: 14, fontWeight: 'bold' } }, data: data, color: palette }] }); ensureSharedResize(); _charts.set(domId, { chart, handler: null }); } function disposeByDomId(domId) { const entry = _charts.get(domId); if (!entry) return; try { entry.chart.dispose(); } catch { /* already disposed */ } _charts.delete(domId); removeSharedResizeIfEmpty(); } function disposeAll() { for (const { chart } of _charts.values()) { try { chart.dispose(); } catch { /* already disposed */ } } _charts.clear(); removeSharedResizeIfEmpty(); } /** Re-render all charts (e.g. after tab becomes visible again) */ function resizeAll() { for (const { chart } of _charts.values()) { try { chart.resize(); } catch { /* disposed */ } } } return { ensureECharts, renderChart, renderBarChart, renderDonutChart, disposeByDomId, disposeAll, resizeAll }; })();