几十万条评测结果堆在那里,用户真正要的是一眼能懂的图表。选型阶段我对比了 ECharts、Chart.js、D3.js 和 AntV G2 四家,最后定了 ECharts——图表种类全、大数据渲染强、中文文档完整,跟 Vue 的集成也成熟。这篇把我在项目里用 Vue + ECharts 搭建整套报告体系的过程写下来,包括每个图表为什么这么做、性能怎么优化、以及踩过的坑。
一、为什么选 ECharts
选型不是拍脑袋。我们当时有个硬约束:报告页要同时展示评分分布、多模型雷达对比、耗时折线和成本饼图,这一页最好只用一套图形库搞定。D3.js 灵活但学习曲线陡,团队没人愿意为几类常用图表花一个月上手;Chart.js 轻量,但图表种类和自定义能力偏弱,画雷达图、数据缩放这类交互都得自己折腾。后面我把四家主流方案放在一起做了个横向对比。下面从 5 个维度看一下差异。
| 维度 | ECharts | Chart.js | D3.js | AntV G2 |
|---|---|---|---|---|
| 图表种类 | 100+ | 30+ | 无限制 | 50+ |
| 性能(大数据) | 优 | 中 | 优 | 中 |
| 学习曲线 | 中 | 低 | 高 | 中 |
| 中文文档 | 完整 | 弱 | 弱 | 完整 |
| 社区生态 | 大 | 中 | 大 | 中 |
结论很清楚:ECharts 在”种类全 + 大数据性能 + 中文文档”三点上恰好同时命中我们的需求,而这三点正是评测报告场景最缺的。D3.js 虽然上限高,但我们的业务不需要从零造轮子,维护成本才是长期风险。平台选 ECharts 原因:图表种类全、大数据性能强、中文文档完整、Vue 集成成熟。
二、Vue 3 集成
集成方式我们一开始也纠结过:直接操作 echarts 实例,还是用封装好的 vue-echarts。手动方案自由度大,但要在组件卸载时手动调用 dispose,漏写就是内存泄漏;vue-echarts 官方封装把这些生命周期都管好了,组件卸载自动清理。我们最终选了封装方案。先安装依赖。
npm i echarts vue-echarts
装好之后在入口按需注册要用的图表和组件,代码量不大但很关键。
// main.ts
import { createApp } from 'vue'
import ECharts from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { BarChart, LineChart, PieChart, ScatterChart } from 'echarts/charts'
import {
GridComponent, TooltipComponent, LegendComponent,
DataZoomComponent, TitleComponent, MarkLineComponent
} from 'echarts/components'
use([
CanvasRenderer, BarChart, LineChart, PieChart, ScatterChart,
GridComponent, TooltipComponent, LegendComponent,
DataZoomComponent, TitleComponent, MarkLineComponent
])
app.component('VChart', ECharts)
按需引入的作用很直接:全量 echarts 包体积在 1MB 左右,用 use() 只注册用到的图表和组件后,压到 400KB,首屏加载肉眼可见地快了。之后模板里就能直接用 <VChart> 组件,配一个 option 就能出图。
三、平台核心图表实现
平台报告页的四类核心图表,我按实现难度从简单到复杂逐个说。它们覆盖了评测结果从”分布”到”对比”再到”趋势”的完整视角。
1. 评分分布直方图
评分分布是报告页第一个视图,把 0-10 分切成十个桶,统计每个区间的答案数。这里有个细节:分数可能是小数,Math.min(9, Math.floor(it.score)) 做一次钳制,避免 10 分落进第 9 个桶。
<template>
<v-chart :option="chartOption" autoresize />
</template>
<!-- 模板逻辑段 (TS) -->
import { computed } from 'vue'
import { useEvalStore } from '@/stores/eval'
const props = defineProps<{ sessionId: number }>()
const evalStore = useEvalStore()
const chartOption = computed(() => {
const items = evalStore.getItemsBySession(props.sessionId)
const buckets = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // 0-1, 1-2, ..., 9-10
items.forEach(it => {
if (it.score !== null) {
const idx = Math.min(9, Math.floor(it.score))
buckets[idx]++
}
})
return {
xAxis: { type: 'category', data: ['0-1', '1-2', ..., '9-10'] },
yAxis: { type: 'value', name: '答案数' },
series: [{ type: 'bar', data: buckets,
itemStyle: { color: '#5470c6' } }],
tooltip: { trigger: 'axis' }
}
})
<!-- 模板结束 -->
图表用 computed 驱动,store 里的数据一更新,option 自动重建,开发者完全不用手动调 setOption。这套写法在流式评测里尤其省心——每来一条答案,直方图自动多一根柱子。
2. 多模型对比雷达图
多模型对比我们选了雷达图而不是柱状图,因为五个维度(准确性、完整性、可用性、相关性、创新性)要看”形状”,雷达图一眼能看出哪个模型偏科、哪个全面。
const radarOption = {
radar: {
indicator: [
{ name: '准确性', max: 10 },
{ name: '完整性', max: 10 },
{ name: '可用性', max: 10 },
{ name: '相关性', max: 10 },
{ name: '创新性', max: 10 }
]
},
series: [{
type: 'radar',
data: [
{ name: 'GPT-4o', value: [9, 8, 9, 8, 7] },
{ name: 'Claude', value: [9, 9, 8, 7, 8] },
{ name: 'DeepSeek', value: [7, 8, 7, 8, 6] }
]
}]
}
注意 indicator 的 max 要统一,否则不同维度量纲不一致,雷达形状会被拉伸变形。我们早期就犯过这个错,五个维度 max 不统一,图一出来就是畸形的多边形,对不上评分数据。
3. 评测时长折线图
评测时长按时间轴画折线,能看到平台一天内的响应波动。加上面积填充,低谷高峰一目了然,适合监控用。
const lineOption = {
xAxis: { type: 'time' },
yAxis: { type: 'value', name: '秒' },
series: [{
type: 'line',
smooth: true,
areaStyle: {},
data: items.map(it => [it.createdAt, it.durationMs / 1000])
}]
}
xAxis 用 time 类型后,ECharts 会自动做时间刻度的切分,跨天的数据不用自己算刻度,放进去就是合法的 [时间, 值] 二元组数组。smooth 开启后曲线过渡更顺,观感比折线好不少。
4. 成本分布饼图
成本饼图是运营最关心的图——不同模型调用成本占比。用了环形样式(radius 从 40% 到 70%),中间留白可以放总数文字,观感更现代。
const pieOption = {
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: [
{ name: 'GPT-4o', value: 124.5 },
{ name: 'Claude', value: 89.3 },
{ name: 'DeepSeek', value: 23.1 }
]
}]
}
环形图比实心饼图看起来清爽,hover 出来占比,管理员核对账单时很直观。唯一要提醒的是饼图数据量少才好看,超过六七个分类建议归并成”其他”,否则图例挤成一团。
四、大数据性能优化
评测历史动辄上万条,直接全量渲染会卡。这一节的四个优化我们按性价比排了序:采样、渲染器、增量更新、关动画,从最省事到最彻底。
1. 数据采样
上万点的折线直接画,Canvas 绘制负担重。我们写了个简单的等间隔采样函数。
function downsample(data: Point[], maxPoints: number): Point[] {
if (data.length <= maxPoints) return data
const step = Math.ceil(data.length / maxPoints)
return data.filter((_, i) => i % step === 0)
}
1 万点 → 500 点显示,肉眼无差但渲染快 20 倍。
注意采样要保留首尾点,否则曲线端点会漂移。等间隔采样是性价比很高的方式,几行代码就把渲染压力降了一个量级。
2. 渲染器选择
ECharts 支持 Canvas 和 SVG 两种渲染器。大数据量选 Canvas,渲染快;数据量小或移动端选 SVG,清晰度高,缩放不糊。
import { CanvasRenderer, SVGRenderer } from 'echarts/renderers'
// 大数据量用 Canvas
chart.setOption(option, false, true) // lazyUpdate
// 移动端/数据量小用 SVG(更清晰)
use(SVGRenderer)
同一个图可以同时注册两种渲染器,运行时按场景切换。我们桌面端默认 Canvas,移动端报告页用 SVG,兼顾性能和清晰度。lazyUpdate 这个参数也值得提,它会把多次 setOption 合并成一次渲染,连续推送数据时很有用。
3. 增量更新
流式评测场景下,数据会持续追加。每次 setOption 全量替换会触发整体重绘,浪费又不流畅。
// 错:每次 setOption 重新渲染
chart.setOption(newOption)
// 对:只更新变化部分
chart.setOption({
series: [{ data: newData }]
})
只传变化的部分,ECharts 会做 diff 合并,重绘范围小很多。流式输出时分数一列列冒出来,靠的就是这个机制兜底,浏览器主线程不会被整页重绘占满。
4. 关闭动画
动画在数据量大的时候是纯负担,好看但费性能。数据点超过阈值后我们直接关动画,并开启大数据模式。
const option = {
animation: false, // 大数据关动画
series: [{ type: 'bar', data, large: true, largeThreshold: 1000 }]
}
large: true 配合 largeThreshold,ECharts 会走大规模渲染的优化路径,散点图 5000 点以上时效果特别明显,拖动和缩放不再掉帧。
五、与 Vue 响应式集成
Vue 和 ECharts 集成的核心是”option 响应式”。我们踩过的坑是 option 用普通对象浅引用,Vue 的 watch 根本不会触发。后来统一改成 ref 包裹、整体替换,图表才跟着数据动起来。
<template>
<!-- autoresize 监听容器尺寸变化自适应 -->
<v-chart :option="option" autoresize
@click="onChartClick" />
</template>
<!-- 模板逻辑段 (TS) -->
import { ref, watch } from 'vue'
const props = defineProps<{ data: any[] }>()
const option = ref({})
watch(() => props.data, (newData) => {
option.value = buildChartOption(newData)
}, { immediate: true })
autoresize 必备:窗口大小变化时图表自动重绘。还有一个细节:容器处于 display:none 状态时图表高度会是 0,切回可见时要手动触发 resize,这个坑后面第九节会专门讲。
六、图表交互
报告页的图表不是静态展示,用户要能点进去看详情。交互主要是三类:点击、悬浮、缩放,每类都有值得注意的边界情况。
点击事件
点击某个数据点跳转到对应的评测 session 详情页,是报告页最常见的一类交互。
function onChartClick(params: any) {
if (params.componentType === 'series') {
const sessionId = params.data.sessionId
router.push(`/eval/sxs/${sessionId}`)
}
}
注意先判断 componentType === 'series',避免点到坐标轴、图例上也触发跳转,这个判断能省掉一堆莫名其妙的 bug 反馈。
鼠标悬浮高亮
tooltip 默认把所有系列的信息都堆出来,观感杂乱。我们用 formatter 自定义成”时间 + 分数”的紧凑格式。
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const p = params[0]
return `时间: ${p.axisValue}<br/>分数: ${p.data}`
}
}
formatter 里返回的字符串可以带 HTML 结构,但要注意特殊字符转义。我们只输出数字和时间这类受控内容,避免把模型输出的文本直接拼进 tooltip。
数据缩放
长周期数据既要看全貌,又要能放大细节。dataZoom 两种类型叠加,体验正好互补。
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ type: 'slider' }
]
inside 支持滚轮缩放和拖拽平移,slider 提供可视的拖拽条。两者联动的效果是:拖动滑块,图表视图同步移动,大范围时间数据浏览体验好很多。
七、图表导出
报告要能下载,这是产品的硬需求。ECharts 自带的 getDataURL 可以把当前图表导出成图片,配合高清比例参数,打印和存档都够用。
// 导出为图片
chart.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#fff'
})
// 导出为 PDF(在父组件中用 jspdf 包装)
导出 PDF 我们用 jspdf 把 base64 图片塞进页面再生成。平台把”导出报告”做成按钮,一键下载 PNG 加数据 JSON,运营存档和对外分享都靠它。
八、自定义主题
默认主题是蓝色系,跟产品调性不搭。我们注册了自己的主题,颜色、背景、字体统一管理,报告页视觉风格才立得住。
// echarts-theme.ts
export const evalTheme = {
color: ['#5470c6', '#91cc75', '#fac858', '#ee6666'],
backgroundColor: 'transparent',
textStyle: { fontFamily: 'PingFang SC, sans-serif' }
}
// 引入
import evalTheme from './echarts-theme'
use(EChartsTheme, evalTheme)
主题接入 CSS 变量后,dark mode 下图表颜色能跟着页面主题切换,不再出现深色背景里图表看不清的问题。字体也值得定死,服务端渲染或 Linux 环境缺中文字体时,图表文字会变成方块。
九、踩过的坑
图表这块的坑,一半在响应式,一半在生命周期,列出来给大家排雷。每一条我们都在线上或测试环境真实遇到过。
- 图表不更新:option 是浅引用,Vue 不会触发 watch。改用
ref({})并整体替换。 - 响应式尺寸失效:父容器
display:none时图表 0 高度。用 ResizeObserver 监听。 - 大数据点重叠:散点图 > 5000 点全堆一起。改用
large: true大数据模式。 - 多次创建实例:组件卸载时 ECharts 实例不释放会内存泄漏。用
vue-echarts自动管理或手动chart.dispose()。 - 颜色看不清:主题色硬编码遇到 dark mode 看不见。用 CSS 变量动态取色。
- tooltip 中文乱码:默认字体在 Linux 不带中文。
textStyle: { fontFamily: '...中文...' }。
这些坑的共同点是:图表是典型的”副作用型”组件,生命周期和尺寸管理不当就会出各种怪问题,用 vue-echarts 这类封装能省一大半的心。
十、报告页面最佳实践
报告页最终布局分四区,从”结论”到”证据”层层递进,用户扫一眼就能抓住重点。
- 顶部概览:6 个 KPI 卡片(总数/通过率/平均分/最高分/最低分/总成本)
- 中部分布:评分直方图 + 雷达图
- 底部明细:可排序的答案列表
- 侧边操作:导出 PDF / 分享链接 / 二次评分
布局骨架用 Element Plus 的栅格系统搭,KPI 一行、图表一行。
<template>
<el-row :gutter="20">
<el-col :span="4" v-for="kpi in kpis" :key="kpi.key">
<KpiCard :title="kpi.title" :value="kpi.value"
:delta="kpi.delta" />
</el-col>
</el-row>
<el-row :gutter="20" class="mt-20">
<el-col :span="12">
<v-chart :option="histogramOption" autoresize
style="height: 360px" />
</el-col>
<el-col :span="12">
<v-chart :option="radarOption" autoresize
style="height: 360px" />
</el-col>
</el-row>
</template>
这套布局上线后,用户反馈最集中的一句话是”终于能看懂评测结果了”。KPI 卡片给结论,图表给分布,明细给证据,三层信息从粗到细,报告的意义才真正落地。
常见问题(FAQ)
Q1:ECharts 和 Chart.js 怎么选?
ECharts 功能强,适合中后台复杂图表;Chart.js 轻量,适合简单展示。平台选 ECharts 因为图表种类需要多。
Q2:3D 图表支持吗?
支持,但要引入 echarts-gl。平台暂不需要 3D。
Q3:怎么在 Vue 里避免 ECharts 内存泄漏?
用 vue-echarts 它在组件 unmount 时自动 dispose()。手动初始化时一定要在 onUnmounted 里 chart.dispose()。