/** * EwoooC Chart Theme v3.0 — Production * ───────────────────────────────────────────────────────────── * 變更重點(相對 v2.x): * 1. 動態讀取當前頁面 [data-page-group],圖表自動換群組調色盤 * 2. 移除 dotMatrixPlugin(chart 底圖點陣)— 干擾資料判讀 * 3. 移除 alpha fill 預設(散布圖、長條圖背景透明度過低看不清) * 4. 字體統一走 var(--momo-font-mono) for tick / value * 5. 顏色映射保留 — 第三方 chart config 用冷色時自動轉暖色 * * 群組調色盤: * monitor → caramel primary,apricot/honey/olive/clay/greige * analytics → honey primary,caramel/olive/apricot/clay/greige * ops → clay primary,terra/honey/caramel/apricot/greige * ai → saffron primary,honey/caramel/olive/apricot/greige * system → terra primary,clay/olive/greige/honey/apricot */ (function () { const root = document.documentElement; const css = (name, fallback) => getComputedStyle(root).getPropertyValue(name).trim() || fallback; function readPalette() { // 優先讀群組變數,沒有就 fallback 到 momo-warm-* const shell = document.querySelector('.momo-app[data-page-group]'); const scope = shell || document.documentElement; const get = (n, fb) => getComputedStyle(scope).getPropertyValue(n).trim() || fb; return [ get('--momo-page-chart-1', '#c96442'), get('--momo-page-chart-2', '#e7b98f'), get('--momo-page-chart-3', '#c89043'), get('--momo-page-chart-4', '#8a7340'), get('--momo-page-chart-5', '#b86f52'), get('--momo-page-chart-6', '#b8aea2'), '#a85d3d', '#6f6256', '#d8c1aa', '#edd6cc' ]; } const theme = { font: css('--momo-font-family', '"Inter", "Noto Sans TC", sans-serif'), mono: css('--momo-font-mono', '"JetBrains Mono", monospace'), ink: css('--momo-text-primary', '#2a2520'), muted: css('--momo-text-secondary', '#6b6155'), faint: 'rgba(42, 37, 32, 0.10)', paper: css('--momo-bg-paper', '#f5efe2'), elevated: css('--momo-bg-elevated', '#fdfaf2') }; const isCompact = () => window.matchMedia && window.matchMedia('(max-width: 768px)').matches; const clamp = (v, mn, mx) => Math.max(mn, Math.min(mx, v)); // 冷色 → 暖色映射(保留 v2 行為) const coldColorMap = { '#6366f1': '#c96442', '#3b82f6': '#4a6b85', '#8b5cf6': '#8f4530', '#10b981': '#5a7a3f', '#14b8a6': '#5a7a3f', '#0ea5e9': '#4a6b85', '#0284c7': '#4a6b85', '#94a3b8': '#8f857a', '#d1d5db': '#c7bcae', '#ec4899': '#b5342f', '#ef4444': '#b5342f', '#f59e0b': '#c89043' }; const alpha = (hex, amount) => { if (!hex || !hex.startsWith('#') || hex.length < 7) return hex; const n = parseInt(hex.slice(1, 7), 16); return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${amount})`; }; function mapColor(color, fb) { if (typeof color !== 'string') return color || fb; const k = color.trim().toLowerCase(); return coldColorMap[k] || color; } function formatNumber(v) { if (typeof v !== 'number' || !Number.isFinite(v)) return v; return Math.abs(v) >= 10000 ? `${Math.round(v / 10000).toLocaleString()}萬` : v.toLocaleString(); } function tuneDataset(ds, idx, type) { if (!ds || typeof ds !== 'object') return; const palette = readPalette(); const color = palette[idx % palette.length]; const t = ds.type || type; if (t === 'doughnut' || t === 'pie' || t === 'polarArea') { ds.backgroundColor = palette.map(c => alpha(c, 0.85)); ds.borderColor = theme.elevated; ds.borderWidth = 2; return; } if (typeof ds.backgroundColor !== 'function') { ds.backgroundColor = t === 'line' ? alpha(color, ds.fill ? 0.18 : 0.0) : alpha(color, 0.78); } if (typeof ds.borderColor !== 'function') ds.borderColor = color; ds.borderWidth = ds.borderWidth || (t === 'line' ? 2 : 1); ds.borderRadius = ds.borderRadius ?? (t === 'bar' ? 3 : undefined); ds.borderSkipped = ds.borderSkipped ?? (t === 'bar' ? false : undefined); ds.pointRadius = ds.pointRadius ?? (t === 'line' ? 2 : undefined); ds.pointHoverRadius = ds.pointHoverRadius ?? (t === 'line' ? 4 : undefined); ds.pointBackgroundColor = ds.pointBackgroundColor || color; ds.pointBorderColor = ds.pointBorderColor || theme.elevated; ds.tension = ds.tension ?? (t === 'line' ? 0.32 : undefined); } function tuneChartConfig(config) { if (!config || typeof config !== 'object') return config; const type = config.type || 'bar'; const datasets = config.data && Array.isArray(config.data.datasets) ? config.data.datasets : []; datasets.forEach((d, i) => tuneDataset(d, i, type)); config.options = config.options || {}; config.options.responsive = config.options.responsive !== false; config.options.maintainAspectRatio = false; const plugins = (config.options.plugins = config.options.plugins || {}); plugins.legend = { ...(plugins.legend || {}), labels: { color: theme.muted, boxWidth: 10, boxHeight: 10, borderRadius: 2, padding: 12, font: { family: theme.mono, size: 11, weight: '600' }, ...((plugins.legend || {}).labels || {}) } }; plugins.tooltip = { ...(plugins.tooltip || {}), backgroundColor: theme.elevated, titleColor: theme.ink, bodyColor: theme.ink, borderColor: theme.faint, borderWidth: 1, cornerRadius: 4, titleFont: { family: theme.mono, weight: '700', size: 12 }, bodyFont: { family: theme.font, weight: '500', size: 12 }, padding: 10, callbacks: { label: ctx => { const lbl = ctx.dataset && ctx.dataset.label ? `${ctx.dataset.label}: ` : ''; const v = typeof ctx.parsed === 'object' ? ctx.parsed.y ?? ctx.parsed.x ?? ctx.raw : ctx.parsed; return `${lbl}${formatNumber(v)}`; }, ...((plugins.tooltip || {}).callbacks || {}) } }; const scales = config.options.scales || {}; Object.keys(scales).forEach(k => { scales[k] = { border: { color: theme.faint, ...(scales[k].border || {}) }, grid: { color: theme.faint, tickColor: 'transparent', drawBorder: false, ...(scales[k].grid || {}) }, ticks: { color: theme.muted, maxRotation: isCompact() ? 0 : 30, autoSkip: isCompact(), font: { family: theme.mono, size: isCompact() ? 10 : 11, weight: '500' }, callback: v => formatNumber(v), ...(scales[k].ticks || {}) }, ...scales[k] }; }); config.options.scales = scales; return config; } function installChartJsTheme() { if (!window.Chart || window.Chart.__ewooocThemed) return; const Native = window.Chart; Native.defaults.font.family = theme.font; Native.defaults.font.size = 12; Native.defaults.color = theme.muted; function ThemedChart(item, config) { return new Native(item, tuneChartConfig(config)); } for (const k of Reflect.ownKeys(Native)) { if (['length', 'name', 'prototype'].includes(k)) continue; try { Object.defineProperty( ThemedChart, k, Object.getOwnPropertyDescriptor(Native, k) ); } catch (e) {} } ThemedChart.prototype = Native.prototype; Object.setPrototypeOf(ThemedChart, Native); ThemedChart.__ewooocThemed = true; window.Chart = ThemedChart; } // ECharts 走精簡版(v2 完整 mergeAxis/tuneSeries 保留邏輯但用群組色) function installEchartsTheme() { if (!window.echarts || window.echarts.__ewooocThemed) return; const ec = window.echarts; const nativeInit = ec.init.bind(ec); ec.init = function (dom, themeName, opts) { const chart = nativeInit(dom, themeName || null, opts); const setOpt = chart.setOption.bind(chart); chart.setOption = function (opt, notMerge, lazy) { const palette = readPalette(); const tuned = { ...opt, color: palette, backgroundColor: 'transparent', textStyle: { color: theme.ink, fontFamily: theme.font } }; return setOpt(tuned, notMerge, lazy); }; if (window.ResizeObserver && dom) { new ResizeObserver(() => requestAnimationFrame(() => chart.resize()) ).observe(dom); } return chart; }; ec.__ewooocThemed = true; } window.EwoooCChartTheme = { readPalette, theme, tuneChartConfig, installChartJsTheme, installEchartsTheme }; installChartJsTheme(); installEchartsTheme(); })();