(function () { const root = document.documentElement; const css = (name, fallback) => getComputedStyle(root).getPropertyValue(name).trim() || fallback; const theme = { font: css('--momo-font-family', '"Inter", "Noto Sans TC", sans-serif'), mono: css('--momo-font-family-mono', '"JetBrains Mono", monospace'), ink: css('--momo-text-primary', '#2a2520'), muted: css('--momo-text-secondary', '#645c52'), faint: 'rgba(42, 37, 32, 0.10)', paper: css('--momo-bg-paper', '#f3eee2'), elevated: css('--momo-bg-elevated', '#fdfaf3'), accent: css('--momo-warm-caramel', '#c96442'), honey: css('--momo-warm-honey', '#b88416'), rust: css('--momo-warm-rust', '#b5342f'), mahogany: css('--momo-warm-mahogany', '#8f4530'), earth: css('--momo-warm-earth', '#8a5a2b'), success: css('--momo-success', '#2a7a3f'), info: css('--momo-info', '#2d5d80') }; const palette = [ theme.accent, theme.success, theme.honey, theme.mahogany, theme.info, theme.earth, theme.rust, '#6f6256', '#a66a3f', '#576a42', '#9b6f1c', '#5f4638' ]; const isCompact = () => window.matchMedia && window.matchMedia('(max-width: 768px)').matches; const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); const coldColorMap = { '#6366f1': theme.accent, '#3b82f6': theme.info, '#8b5cf6': theme.mahogany, '#10b981': theme.success, '#14b8a6': theme.success, '#0ea5e9': theme.info, '#0284c7': theme.info, '#94a3b8': '#8f857a', '#d1d5db': '#c7bcae', '#ec4899': theme.rust, '#ef4444': theme.rust, '#f59e0b': theme.honey }; const alpha = (hex, amount) => { if (!hex || !hex.startsWith('#') || hex.length < 7) return hex; const n = parseInt(hex.slice(1, 7), 16); const r = (n >> 16) & 255; const g = (n >> 8) & 255; const b = n & 255; return `rgba(${r}, ${g}, ${b}, ${amount})`; }; function mapColor(color, fallback) { if (typeof color !== 'string') return color || fallback; const key = color.trim().toLowerCase(); if (coldColorMap[key]) return coldColorMap[key]; return color; } function mapPalette(colors) { if (!Array.isArray(colors)) return palette; return colors.map((color, index) => mapColor(color, palette[index % palette.length])); } function formatNumber(value) { if (typeof value !== 'number' || !Number.isFinite(value)) return value; return Math.abs(value) >= 10000 ? `${Math.round(value / 10000).toLocaleString()}萬` : value.toLocaleString(); } function tuneDataset(dataset, index, chartType) { if (!dataset || typeof dataset !== 'object') return; const color = palette[index % palette.length]; const type = dataset.type || chartType; if (type === 'doughnut' || type === 'pie' || type === 'polarArea') { dataset.backgroundColor = palette.map((item) => alpha(item, 0.82)); dataset.borderColor = theme.elevated; dataset.borderWidth = 2; return; } if (type === 'treemap' && typeof dataset.backgroundColor === 'function') { return; } if (typeof dataset.backgroundColor !== 'function') { dataset.backgroundColor = type === 'line' ? alpha(color, dataset.fill ? 0.16 : 0.08) : alpha(color, 0.72); } if (typeof dataset.borderColor !== 'function') { dataset.borderColor = color; } dataset.borderWidth = dataset.borderWidth || (type === 'line' ? 2 : 1); dataset.borderRadius = dataset.borderRadius ?? (type === 'bar' ? 5 : undefined); dataset.borderSkipped = dataset.borderSkipped ?? (type === 'bar' ? false : undefined); dataset.pointRadius = dataset.pointRadius ?? (type === 'line' ? 2.5 : undefined); dataset.pointHoverRadius = dataset.pointHoverRadius ?? (type === 'line' ? 4 : undefined); dataset.pointBackgroundColor = dataset.pointBackgroundColor || color; dataset.pointBorderColor = dataset.pointBorderColor || theme.elevated; dataset.tension = dataset.tension ?? (type === 'line' ? 0.32 : undefined); } function tuneChartConfig(config) { if (!config || typeof config !== 'object') return config; const chartType = config.type || 'bar'; const datasets = config.data && Array.isArray(config.data.datasets) ? config.data.datasets : []; datasets.forEach((dataset, index) => tuneDataset(dataset, index, chartType)); config.options = config.options || {}; config.options.responsive = config.options.responsive !== false; config.options.maintainAspectRatio = false; config.options.resizeDelay = config.options.resizeDelay ?? 90; config.options.interaction = { mode: chartType === 'scatter' || chartType === 'bubble' ? 'nearest' : 'index', intersect: chartType === 'scatter' || chartType === 'bubble', ...(config.options.interaction || {}) }; const plugins = config.options.plugins = config.options.plugins || {}; plugins.legend = { ...(plugins.legend || {}), labels: { color: theme.muted, boxWidth: 10, boxHeight: 10, borderRadius: 2, padding: 14, font: { family: theme.mono, size: 11, weight: '700' }, ...((plugins.legend || {}).labels || {}) } }; plugins.tooltip = { ...(plugins.tooltip || {}), backgroundColor: 'rgba(253, 250, 243, 0.97)', titleColor: theme.ink, bodyColor: theme.ink, borderColor: alpha(theme.accent, 0.38), borderWidth: 1, cornerRadius: 6, displayColors: true, titleFont: { family: theme.mono, weight: '800', size: 12 }, bodyFont: { family: theme.font, weight: '600', size: 12 }, padding: 10, callbacks: { label: function (ctx) { const label = ctx.dataset && ctx.dataset.label ? `${ctx.dataset.label}: ` : ''; const value = typeof ctx.parsed === 'object' ? (ctx.parsed.y ?? ctx.parsed.x ?? ctx.raw) : ctx.parsed; return `${label}${formatNumber(value)}`; }, ...((plugins.tooltip || {}).callbacks || {}) } }; const scales = config.options.scales || {}; Object.keys(scales).forEach((key) => { scales[key] = { border: { color: theme.faint, ...(scales[key].border || {}) }, grid: { color: theme.faint, tickColor: 'transparent', drawBorder: false, ...(scales[key].grid || {}) }, ticks: { color: theme.muted, maxRotation: isCompact() ? 0 : 45, autoSkip: isCompact(), font: { family: theme.mono, size: isCompact() ? 10 : 11, weight: '650' }, callback: function (value) { return formatNumber(value); }, ...(scales[key].ticks || {}) }, title: { color: theme.muted, font: { family: theme.font, size: 12, weight: '800' }, ...(scales[key].title || {}) }, ...scales[key] }; }); config.options.scales = scales; return config; } function mergeAxis(axis) { if (!axis) return axis; const tuneOne = (item) => ({ ...item, axisLine: { ...(item.axisLine || {}), lineStyle: { ...((item.axisLine || {}).lineStyle || {}), color: mapColor(((item.axisLine || {}).lineStyle || {}).color, theme.faint) } }, axisTick: { alignWithLabel: true, ...(item.axisTick || {}) }, splitLine: { ...(item.splitLine || {}), lineStyle: { type: 'dashed', ...((item.splitLine || {}).lineStyle || {}), color: mapColor(((item.splitLine || {}).lineStyle || {}).color, theme.faint) } }, axisLabel: { ...((item.axisLabel || {})), color: theme.muted, fontFamily: theme.mono, fontSize: isCompact() ? 10 : clamp((item.axisLabel || {}).fontSize || 11, 10, 12), fontWeight: 700, hideOverlap: true, overflow: 'truncate', width: (item.axisLabel || {}).width || (isCompact() ? 72 : 120) }, nameTextStyle: { ...((item.nameTextStyle || {})), color: theme.muted, fontFamily: theme.font, fontWeight: 800 } }); return Array.isArray(axis) ? axis.map(tuneOne) : tuneOne(axis); } function tuneItemStyle(itemStyle, index) { const fallback = palette[index % palette.length]; if (typeof itemStyle === 'function') return itemStyle; const tuned = { ...(itemStyle || {}) }; if (typeof tuned.color === 'string') tuned.color = mapColor(tuned.color, fallback); if (!tuned.color) tuned.color = fallback; tuned.opacity = tuned.opacity ?? 0.82; tuned.borderColor = tuned.borderColor || alpha(theme.elevated, 0.94); tuned.borderWidth = tuned.borderWidth ?? 1; return tuned; } function tuneSeries(series) { if (!series) return series; const list = Array.isArray(series) ? series : [series]; const compact = isCompact(); const tuned = list.map((item, index) => { const type = item.type || 'line'; const dataLength = Array.isArray(item.data) ? item.data.length : 0; const label = item.label || {}; const shouldShowLabel = label.show === true && !(compact && (dataLength > 8 || type === 'pie' || type === 'heatmap')); return { ...item, itemStyle: tuneItemStyle(item.itemStyle, index), lineStyle: type === 'line' ? { ...item.lineStyle, color: mapColor((item.lineStyle || {}).color, palette[index % palette.length]), width: (item.lineStyle || {}).width || 2.4 } : item.lineStyle, areaStyle: item.areaStyle ? { ...item.areaStyle, opacity: compact ? 0.08 : 0.12, color: alpha(mapColor((item.areaStyle || {}).color, palette[index % palette.length]), compact ? 0.10 : 0.16) } : item.areaStyle, label: { ...label, color: theme.ink, fontFamily: theme.mono, fontSize: compact ? 10 : clamp(label.fontSize || 11, 10, 12), fontWeight: 800, hideOverlap: true, overflow: 'truncate', show: shouldShowLabel }, labelLine: type === 'pie' ? { length: compact ? 8 : 14, length2: compact ? 6 : 10, ...(item.labelLine || {}) } : item.labelLine, emphasis: { focus: type === 'pie' ? 'self' : 'series', ...(item.emphasis || {}) }, barMaxWidth: type === 'bar' ? (compact ? 18 : (item.barMaxWidth || 28)) : item.barMaxWidth, barCategoryGap: type === 'bar' ? (item.barCategoryGap || '42%') : item.barCategoryGap, symbolSize: type === 'line' ? (item.symbolSize || (compact ? 4 : 5)) : item.symbolSize }; }); return Array.isArray(series) ? tuned : tuned[0]; } function tuneEchartsOption(option) { const compact = isCompact(); const tuned = { ...option, color: option.color ? mapPalette(option.color) : palette, backgroundColor: 'transparent', textStyle: { color: theme.ink, fontFamily: theme.font, ...(option.textStyle || {}) }, grid: { containLabel: true, borderColor: theme.faint, left: compact ? 10 : 28, right: compact ? 12 : 24, top: compact ? 32 : 40, bottom: compact ? 38 : 44, ...(option.grid || {}) }, tooltip: { backgroundColor: 'rgba(253, 250, 243, 0.97)', borderColor: alpha(theme.accent, 0.36), borderWidth: 1, textStyle: { color: theme.ink, fontFamily: theme.font, fontWeight: 650 }, extraCssText: 'box-shadow:0 10px 24px rgba(42,37,32,.12);border-radius:6px;', ...(option.tooltip || {}) }, legend: { ...(option.legend || {}), type: compact ? 'scroll' : ((option.legend || {}).type || 'plain'), textStyle: { ...((option.legend || {}).textStyle || {}), color: theme.muted, fontFamily: theme.mono, fontWeight: 700, fontSize: compact ? 10 : clamp(((option.legend || {}).textStyle || {}).fontSize || 11, 10, 12) }, itemWidth: compact ? 10 : 12, itemHeight: compact ? 7 : 8, pageIconColor: theme.accent, pageTextStyle: { ...((option.legend || {}).pageTextStyle || {}), color: theme.muted, fontFamily: theme.mono } }, title: Array.isArray(option.title) ? option.title.map((title) => ({ ...title, textStyle: { ...((title || {}).textStyle || {}), color: theme.ink, fontFamily: theme.font, fontWeight: 800 } })) : option.title ? { ...option.title, textStyle: { ...((option.title || {}).textStyle || {}), color: theme.ink, fontFamily: theme.font, fontWeight: 800 } } : option.title, xAxis: mergeAxis(option.xAxis), yAxis: mergeAxis(option.yAxis), series: tuneSeries(option.series), visualMap: option.visualMap ? { ...option.visualMap, inRange: { ...((option.visualMap || {}).inRange || {}), color: ['#fbf1d3', '#eac26b', theme.honey, theme.accent, theme.mahogany] }, textStyle: { ...((option.visualMap || {}).textStyle || {}), color: theme.muted, fontFamily: theme.mono, fontWeight: 700 }, itemWidth: compact ? 12 : 16, itemHeight: compact ? 84 : 120 } : option.visualMap }; return tuned; } const dotMatrixPlugin = { id: 'ewooocDotMatrix', beforeDraw(chart) { const area = chart.chartArea; if (!area) return; const ctx = chart.ctx; ctx.save(); ctx.fillStyle = 'rgba(253, 250, 243, 0.72)'; ctx.fillRect(area.left, area.top, area.right - area.left, area.bottom - area.top); ctx.fillStyle = 'rgba(42, 37, 32, 0.055)'; for (let x = Math.floor(area.left) + 8; x < area.right; x += 14) { for (let y = Math.floor(area.top) + 8; y < area.bottom; y += 14) { ctx.fillRect(x, y, 1, 1); } } ctx.restore(); } }; function installChartJsTheme() { if (!window.Chart || window.Chart.__ewooocThemed) return; const NativeChart = window.Chart; if (NativeChart.register) NativeChart.register(dotMatrixPlugin); NativeChart.defaults.font.family = theme.font; NativeChart.defaults.font.size = 12; NativeChart.defaults.color = theme.muted; function ThemedChart(item, config) { return new NativeChart(item, tuneChartConfig(config)); } for (const key of Reflect.ownKeys(NativeChart)) { if (['length', 'name', 'prototype'].includes(key)) continue; try { Object.defineProperty(ThemedChart, key, Object.getOwnPropertyDescriptor(NativeChart, key)); } catch (error) {} } ThemedChart.prototype = NativeChart.prototype; Object.setPrototypeOf(ThemedChart, NativeChart); ThemedChart.__ewooocThemed = true; ThemedChart.__native = NativeChart; window.Chart = ThemedChart; } function installEchartsTheme() { if (!window.echarts || window.echarts.__ewooocThemed) return; const echarts = window.echarts; const nativeInit = echarts.init.bind(echarts); echarts.init = function (dom, themeName, opts) { const chart = nativeInit(dom, themeName || null, opts); const nativeSetOption = chart.setOption.bind(chart); chart.setOption = function (option, notMerge, lazyUpdate) { return nativeSetOption(tuneEchartsOption(option || {}), notMerge, lazyUpdate); }; if (window.ResizeObserver && dom) { const observer = new ResizeObserver(() => window.requestAnimationFrame(() => chart.resize())); observer.observe(dom); } if (window.IntersectionObserver && dom) { const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) window.requestAnimationFrame(() => chart.resize()); }); }, { threshold: 0.1 }); observer.observe(dom); } window.requestAnimationFrame(() => chart.resize()); return chart; }; echarts.__ewooocThemed = true; } window.EwoooCChartTheme = { palette, theme, tuneChartConfig, installChartJsTheme, installEchartsTheme }; installChartJsTheme(); installEchartsTheme(); })();