ECharts 跨页面统一:tooltip/色板/零轴的设计体系
一、问题:每个图表都是"一次性"的
venxine.vip 有 5 个页面使用了 ECharts 图表:
| 页面 | 图表数 | 类型 |
|---|---|---|
| fundtracker | 2 | 多线折线图(指数 + 基金) |
| fundreturns | 2 | 堆叠柱状 + 累计折线 |
| demo7 | 5 | 黄金分析 |
| token/summary | 4 | Token 用量统计 |
| funddata | 3 | 月/周/日视图 |
最初每个图表独立配置,导致: - tooltip 格式不统一(有的用 K/M 简写,有的用完整数字) - 图例位置各异(有的在顶部,有的在底部) - 零轴处理混乱(有的画灰虚线,有的完全没有) - 色板随心所欲(全暖色系导致折线无法区分)
每次新加一个图表都要从零开始配,而且配出来的跟其他页面不一样。
二、设计标准:五条铁律
1. Tooltip 格式
┌─────────────────────────────┐
│ 🧾 合计 +12,345,678 │ ← #F97316 13px bold,数字右对齐
│ 6月18日 │ ← #999 12px
│ 上证 +1,234 │ ← 12px 右对齐,红涨绿跌
│ 深证 -567 │
└─────────────────────────────┘
关键规则:
- 顶部"🧾合计"行置顶,用 #F97316 (owOrange) 加粗
- 日期行放第二行,灰色 #999
- 明细行右对齐,正数红色 #e6332a,负数绿色 #00a870
- 值为 0 的系列跳过不显示
- 不使用 K/M 简写:完整数字 + 千分位逗号
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: 'transparent',
borderRadius: 8,
padding: [10, 14],
textStyle: { fontSize: 13 },
formatter: function(params) {
// 1. 计算合计
var total = params.reduce((s, p) => s + (p.value || 0), 0);
// 2. 构建 HTML:合计行 → 日期行 → 明细行
var html = '<div style="font-weight:700;font-size:13px;color:#F97316;display:flex;justify-content:space-between">'
+ '<span>🧾 合计</span>'
+ '<span>' + total.toLocaleString() + '</span></div>';
html += '<div style="font-size:12px;color:#999;margin:4px 0">' + params[0].axisValue + '</div>';
params.forEach(function(p) {
if (p.value === 0 || p.value == null) return;
var color = p.value > 0 ? '#e6332a' : '#00a870';
html += '<div style="display:flex;justify-content:space-between;margin:2px 0">'
+ '<span>' + p.marker + p.seriesName + '</span>'
+ '<span style="color:' + color + ';font-weight:500">' + p.value.toLocaleString() + '</span></div>';
});
return html;
}
}
2. 零轴处理
不使用灰色虚线分隔正负,用 markArea 做红绿分区着色:
markArea: {
silent: true,
data: [
[{ yAxis: 0, itemStyle: { color: 'rgba(230,51,42,0.04)' } }, { yAxis: 'max' }], // 正区淡红
[{ yAxis: 'min' }, { yAxis: 0, itemStyle: { color: 'rgba(0,168,112,0.04)' } }] // 负区淡绿
]
}
同时零刻度标签只用纯 '0',不加 +、% 符号:
yAxis: {
axisLabel: {
formatter: function(v) { return v === 0 ? '0' : v.toLocaleString(); }
}
}
3. 离散色板
拒绝全暖色系(一片橙红分不清),用 7 色离散色板:
var FUND_COLORS = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452'];
// 蓝 绿 黄 红 青 墨绿 橙
每根折线纯色,无面积填充(areaStyle 不设),线条宽度 2px,symbol: 'none':
series: funds.map(function(name, i) {
return {
name: name,
type: 'line',
data: data[name],
symbol: 'none',
lineStyle: { width: 2, color: FUND_COLORS[i] },
itemStyle: { color: FUND_COLORS[i] }
};
})
4. 聚光灯交互
hover 时高亮当前系列,其余灰化:
tooltip: { trigger: 'axis' },
// 在 series 中:
emphasis: { focus: 'series' },
// 全局:
blur: { itemStyle: { opacity: 0.2 } }
5. 非交易日处理
横轴保留所有日期(含周末),非交易日数据为 null,折线用 connectNulls: true 跨周连桥:
series: [{
connectNulls: true,
data: [1.2, null, null, 1.5, ...] // 周末填 null
}]
横轴边界留 5% 空白防贴边:
xAxis: {
boundaryGap: ['5%', '5%']
}
6. 初始化时机:别在隐藏容器里 init
这是跨页面复用时最容易翻车的一条。echarts.init(dom) 会读取容器当时的宽高,如果容器正好是 display:none(藏在没激活的 Tab、折叠面板、或还没展开的路由里),它量到的尺寸是 0,图表要么不显示,要么挤成一条线。
// ❌ 容器还是 display:none 时 init,尺寸算成 0
var chart = echarts.init(document.getElementById('chart'));
// ✅ 等容器可见了再 init,显示后手动 resize
tab.addEventListener('shown', function() {
if (!chart) chart = echarts.init(dom);
chart.resize();
});
配套还得挂一个窗口 resize 监听,否则拖动浏览器窗口时图表尺寸不会跟着变:
window.addEventListener('resize', function() { chart.resize(); });
fundtracker 和 funddata 都有 Tab 切换,这两条是它们能正常显示的前提。
三、落地:从 fundtracker 到 token 的统一
fundtracker 是最早应用这套标准的页面。fundreturns 跟进时,直接把 fundtracker 的 tooltip formatter 和色板常量复制过去。
token 页面是第三个迁移的,它用的是堆叠柱状图而非折线图,但规则完全兼容:
// token 堆叠柱状图
series: [
{ name: '💾 读取', type: 'bar', stack: 'total', color: '#ff9f00' },
{ name: '📥 输入', type: 'bar', stack: 'total', color: '#ffcc00' },
{ name: '📤 输出', type: 'bar', stack: 'total', color: '#e65100' }
]
同色系但三个层次分明的柱状色板仍然遵循了"可区分"原则,且 tooltip 格式与折线图完全一致。
四、核心原则
原则 1:先设标准,后画图表
新页面新增图表时,不要从 {} 开始配置,先从已有的 tooltip formatter、色板数组、markArea 代码块复制粘贴,再调整细节。
原则 2:标准属于代码,不属记忆
不要依赖"我记得 fundtracker 是这么配的",把标准写成注释放在公共位置,或抽成 JavaScript 函数。
原则 3:一次对齐,终身受益
花 30 分钟统一 5 个页面的图表格式,以后每加一个新图表只需 5 分钟,标准配置直接复用,只改数据和系列名。
五、完整配置模板
以下是可以直接复制到任何 ECharts 图表的骨架配置:
var COLOR_PALETTE = ['#5470c6','#91cc75','#fac858','#ee6666','#73c0de','#3ba272','#fc8452'];
var chart = echarts.init(dom);
chart.setOption({
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255,255,255,0.95)',
borderRadius: 8,
padding: [10, 14],
textStyle: { fontSize: 13 },
formatter: function(params) {
var total = params.reduce(function(s,p){ return s + (p.value||0); }, 0);
var h = '<div style="font-weight:700;font-size:13px;color:#F97316;display:flex;justify-content:space-between">'
+ '<span>🧾 合计</span><span>' + total.toLocaleString() + '</span></div>';
h += '<div style="font-size:12px;color:#999;margin:4px 0">' + params[0].axisValue + '</div>';
params.forEach(function(p){
if (!p.value) return;
var c = p.value > 0 ? '#e6332a' : '#00a870';
h += '<div style="display:flex;justify-content:space-between;margin:2px 0">'
+ '<span>' + p.marker + ' ' + p.seriesName + '</span>'
+ '<span style="color:' + c + ';font-weight:500">' + p.value.toLocaleString() + '</span></div>';
});
return h;
}
},
grid: { left: 55, right: 20, top: 32, bottom: 45 },
xAxis: {
type: 'category',
boundaryGap: ['5%','5%'],
axisLabel: { fontSize: 10, color: '#aeaeb2' }
},
yAxis: {
type: 'value',
axisLabel: { fontSize: 11, color: '#aeaeb2', formatter: function(v){ return v===0?'0':v.toLocaleString(); } }
},
legend: { top: 0, itemWidth: 8, itemHeight: 8, borderRadius: 4, textStyle: { fontSize: 11, color: '#5a5a5e' } }
});