419 lines
16 KiB
JavaScript
419 lines
16 KiB
JavaScript
(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();
|
|
})();
|