Files
ewoooc/web/static/js/analysis-chart-theme.js
OoO 81f4a0d18a
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
perf: 延後載入業績圖表資源
2026-05-18 08:51:09 +08:00

289 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* EwoooC Chart Theme v3.0 — Production
* ─────────────────────────────────────────────────────────────
* 變更重點(相對 v2.x
* 1. 動態讀取當前頁面 [data-page-group],圖表自動換群組調色盤
* 2. 移除 dotMatrixPluginchart 底圖點陣)— 干擾資料判讀
* 3. 移除 alpha fill 預設(散布圖、長條圖背景透明度過低看不清)
* 4. 字體統一走 var(--momo-font-mono) for tick / value
* 5. 顏色映射保留 — 第三方 chart config 用冷色時自動轉暖色
*
* 群組調色盤:
* monitor → caramel primaryapricot/honey/olive/clay/greige
* analytics → honey primarycaramel/olive/apricot/clay/greige
* ops → clay primaryterra/honey/caramel/apricot/greige
* ai → saffron primaryhoney/caramel/olive/apricot/greige
* system → terra primaryclay/olive/greige/honey/apricot
*/
(function () {
let chartJsLoader = null;
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;
}
function loadChartJs() {
if (window.Chart) {
installChartJsTheme();
return Promise.resolve(window.Chart);
}
if (chartJsLoader) return chartJsLoader;
chartJsLoader = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js';
script.async = true;
script.onload = () => {
installChartJsTheme();
resolve(window.Chart);
};
script.onerror = () => reject(new Error('Chart.js 載入失敗'));
document.head.appendChild(script);
});
return chartJsLoader;
}
// 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,
loadChartJs,
installChartJsTheme,
installEchartsTheme
};
installChartJsTheme();
installEchartsTheme();
})();