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' } }
});