Files
re-xxm/reaction_board/web/static/charts.js
T

190 lines
5.3 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.
import { formatAxis, formatCount, formatDay } from "./format.js";
const charts = new Map();
let resizeObserver = null;
function colors() {
const styles = getComputedStyle(document.documentElement);
return {
text: styles.getPropertyValue("--text").trim() || "#2a201a",
muted: styles.getPropertyValue("--muted").trim() || "#7b6a58",
grid: styles.getPropertyValue("--grid").trim() || "#eadfce",
accent: styles.getPropertyValue("--accent").trim() || "#a9702f",
blue: styles.getPropertyValue("--blue").trim() || "#5f7f96",
success: styles.getPropertyValue("--success").trim() || "#5f7a4e",
danger: styles.getPropertyValue("--danger").trim() || "#b4553f",
surface: styles.getPropertyValue("--surface").trim() || "#fffdf9",
};
}
export function themeColor(name) {
return colors()[name] || colors().accent;
}
function ensureEcharts() {
if (!window.echarts) {
throw new Error("ECharts 未加载,请检查 /static/vendor/echarts.min.js");
}
return window.echarts;
}
function mount(element, option) {
const echarts = ensureEcharts();
let chart = charts.get(element);
if (!chart) {
chart = echarts.init(element, null, { renderer: "canvas" });
charts.set(element, chart);
}
chart.setOption(option, true);
if (!resizeObserver && typeof ResizeObserver !== "undefined") {
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
charts.get(entry.target)?.resize();
}
});
}
resizeObserver?.observe(element);
return chart;
}
export function disposeAll() {
for (const chart of charts.values()) {
try {
chart.dispose();
} catch {
// 页面切换时忽略已销毁实例
}
}
charts.clear();
resizeObserver?.disconnect();
resizeObserver = null;
}
export function lineChart(element, { dates, values, name = "数值", color, scale = false }) {
if (!element) return null;
const theme = colors();
const lineColor = color || theme.accent;
const option = {
animationDuration: 300,
backgroundColor: "transparent",
grid: { left: 8, right: 12, top: 22, bottom: 8, containLabel: true },
tooltip: {
trigger: "axis",
backgroundColor: theme.surface,
borderColor: theme.grid,
textStyle: { color: theme.text, fontSize: 12 },
formatter(params) {
const point = params[0];
if (!point) return "";
const value = point.value;
return `<strong>${point.axisValue}</strong><br>${name}:${
value === null || value === undefined ? "缺失" : formatCount(value)
}`;
},
},
xAxis: {
type: "category",
data: dates,
boundaryGap: false,
axisLine: { lineStyle: { color: theme.grid } },
axisTick: { show: false },
axisLabel: {
color: theme.muted,
fontSize: 11,
formatter: (value) => formatDay(value),
hideOverlap: true,
},
},
yAxis: {
type: "value",
scale,
splitLine: { lineStyle: { color: theme.grid, type: "dashed" } },
axisLabel: {
color: theme.muted,
fontSize: 11,
formatter: (value) => formatAxis(value),
},
},
series: [{
name,
type: "line",
data: values,
smooth: false,
connectNulls: false,
symbol: "circle",
symbolSize: 5,
showSymbol: values.length <= 45,
lineStyle: { width: 2.2, color: lineColor },
itemStyle: { color: lineColor },
areaStyle: {
color: {
type: "linear", x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: `${lineColor}55` },
{ offset: 1, color: `${lineColor}00` },
],
},
},
}],
};
return mount(element, option);
}
export function barChart(element, { labels, values, name = "数值", color }) {
if (!element) return null;
const theme = colors();
const barColor = color || theme.blue;
const option = {
animationDuration: 300,
backgroundColor: "transparent",
grid: { left: 8, right: 12, top: 22, bottom: 8, containLabel: true },
tooltip: {
trigger: "axis",
axisPointer: { type: "shadow" },
backgroundColor: theme.surface,
borderColor: theme.grid,
textStyle: { color: theme.text, fontSize: 12 },
formatter(params) {
const point = params[0];
if (!point) return "";
return `<strong>${point.name}</strong><br>${name}:${formatCount(point.value)}`;
},
},
xAxis: {
type: "category",
data: labels,
axisLine: { lineStyle: { color: theme.grid } },
axisTick: { show: false },
axisLabel: {
color: theme.muted,
fontSize: 10,
interval: 0,
rotate: labels.length > 8 ? 32 : 0,
formatter: (value) => String(value).slice(0, 10),
},
},
yAxis: {
type: "value",
splitLine: { lineStyle: { color: theme.grid, type: "dashed" } },
axisLabel: {
color: theme.muted,
fontSize: 11,
formatter: (value) => formatAxis(value),
},
},
series: [{
name,
type: "bar",
data: values,
barMaxWidth: 26,
itemStyle: { color: barColor, borderRadius: [4, 4, 0, 0] },
}],
};
return mount(element, option);
}
export function emptyChart(element, message = "暂无数据") {
if (!element) return;
element.innerHTML = `<div class="chart-empty">${message}</div>`;
}