前端可视化家庭账单:用 ECharts 实现支出统计与趋势分析
在家庭财务管理中,直观地看懂钱花到了哪里、花得是否稳定,是提高消费意识与优化预算的关键。本文以 ECharts 为核心,构建一个可视化的家庭账单分析:包括支出分类统计、月度趋势分析、交互筛选与性能优化建议,帮助你在浏览器端快速落地一个实用的可视化面板。
适用场景
- 需要按类别统计支出占比并快速定位高频支出项
- 需要观察月度支出变化趋势并识别异常波动
- 希望在不引入后端的前提下,完成本地或前端的数据分析与展示
数据模型设计
为后续统计与可视化,建议将每笔账单设计为结构化数据:
json
[
{
"date": "2025-01-03",
"category": "餐饮",
"amount": 56.5,
"paymentMethod": "信用卡",
"note": "外卖"
}
]
关键字段说明:
date:YYYY-MM-DD字符串,便于按月聚合category:分类名称,例如餐饮、交通、居住、教育、医疗、娱乐等amount:支出金额,统一为正数paymentMethod:支付方式,按需筛选或做子维度统计
基础搭建
选择纯前端页面即可运行,使用 CDN 引入 ECharts:
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>家庭账单可视化</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5"></script>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.card { background: #fff; border: 1px solid #eee; border-radius: 8px; padding: 8px; }
.title { font-weight: 600; margin: 8px 0; }
.chart { height: 320px; }
</style>
</head>
<body>
<div class="grid">
<div class="card">
<div class="title">支出分类占比</div>
<div id="chart-pie" class="chart"></div>
</div>
<div class="card">
<div class="title">月度支出趋势</div>
<div id="chart-line" class="chart"></div>
</div>
</div>
<script>
const bills = [
{ date: '2025-01-03', category: '餐饮', amount: 56.5, paymentMethod: '信用卡' },
{ date: '2025-01-05', category: '交通', amount: 18, paymentMethod: '现金' },
{ date: '2025-01-08', category: '居住', amount: 2200, paymentMethod: '转账' },
{ date: '2025-02-01', category: '餐饮', amount: 78.2, paymentMethod: '信用卡' },
{ date: '2025-02-06', category: '娱乐', amount: 120, paymentMethod: '信用卡' },
{ date: '2025-02-09', category: '交通', amount: 16, paymentMethod: '现金' },
{ date: '2025-03-02', category: '餐饮', amount: 65.1, paymentMethod: '信用卡' },
{ date: '2025-03-17', category: '教育', amount: 320, paymentMethod: '转账' },
{ date: '2025-03-26', category: '医疗', amount: 180, paymentMethod: '信用卡' },
{ date: '2025-03-28', category: '居住', amount: 2200, paymentMethod: '转账' }
];
function parseMonth(dateStr) {
const d = new Date(dateStr);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
return `${y}-${m}`;
}
function sumByCategory(list) {
const map = new Map();
for (const b of list) {
map.set(b.category, (map.get(b.category) || 0) + b.amount);
}
return Array.from(map, ([category, total]) => ({ category, total }));
}
function sumByMonth(list) {
const map = new Map();
for (const b of list) {
const key = parseMonth(b.date);
map.set(key, (map.get(key) || 0) + b.amount);
}
return Array.from(map, ([month, total]) => ({ month, total })).sort((a, b) => a.month.localeCompare(b.month));
}
const pieChart = echarts.init(document.getElementById('chart-pie'));
const lineChart = echarts.init(document.getElementById('chart-line'));
const categoryTotals = sumByCategory(bills);
const pieOption = {
tooltip: {},
legend: { top: 'bottom' },
series: [
{
type: 'pie',
radius: ['40%', '70%'],
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
data: categoryTotals.map(o => ({ name: o.category, value: Number(o.total.toFixed(2)) }))
}
]
};
const monthTotals = sumByMonth(bills);
const lineOption = {
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: monthTotals.map(o => o.month) },
yAxis: { type: 'value' },
dataZoom: [{ type: 'inside' }, { type: 'slider' }],
series: [
{
name: '月支出',
type: 'line',
smooth: true,
showSymbol: false,
areaStyle: { opacity: 0.2 },
data: monthTotals.map(o => Number(o.total.toFixed(2)))
}
]
};
pieChart.setOption(pieOption);
lineChart.setOption(lineOption);
window.addEventListener('resize', function () {
pieChart.resize();
lineChart.resize();
});
</script>
</body>
</html>
要点:
- 使用
Map做聚合,减少中间对象的开销 - 饼图展示分类占比,折线图展示月度趋势
- 开启
dataZoom,兼顾短期与长期数据的浏览体验
支出统计:类别分布
- 将所有账单按
category聚合求和,并按需排序 - 饼图适合看比例结构,若类别较多可切换为水平条形图以增强可读性
- 可配合
legend、selected实现类别筛选
趋势分析:月度变化
- 依据
date转换成YYYY-MM进行月度聚合 - 折线图的
smooth能提升趋势观感,搭配areaStyle强化视觉层次 - 可在异常峰值处使用
markPoint或visualMap进行突出标记
交互增强
- 时间维度筛选:按年、按月或自定义区间筛选并重新渲染
- 类别筛选:使用图例勾选或下拉框控制类别数据是否参与统计
- 多图联动:点击饼图某分类时,联动折线图仅展示该分类在各月的趋势
性能与数据质量
- 数据量较大时,尽量在聚合前做去噪与无效记录过滤
- 前端聚合建议使用原生结构与一次遍历完成,避免多次 map/reduce 叠加
- 以
dataset统一数据源可降低多图表的重复数据转换成本
扩展建议
- 叠加预算线:在折线图上叠加每月预算阈值,超出则高亮
- 子维度细分:同一类别按
paymentMethod分组,观察支付方式的偏好 - 导出报表:将聚合结果导出为 CSV,便于长期归档
完整示例(含类别联动)
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/echarts@5"></script>
<style>
.toolbar { margin-bottom: 12px; }
.chart { height: 300px; }
</style>
</head>
<body>
<div class="toolbar">
<select id="categoryFilter">
<option value="all">全部类别</option>
<option>餐饮</option>
<option>交通</option>
<option>居住</option>
<option>娱乐</option>
<option>教育</option>
<option>医疗</option>
</select>
</div>
<div id="pie" class="chart"></div>
<div id="line" class="chart"></div>
<script>
const bills = [
{ date: '2025-01-03', category: '餐饮', amount: 56.5 },
{ date: '2025-01-05', category: '交通', amount: 18 },
{ date: '2025-01-08', category: '居住', amount: 2200 },
{ date: '2025-02-01', category: '餐饮', amount: 78.2 },
{ date: '2025-02-06', category: '娱乐', amount: 120 },
{ date: '2025-02-09', category: '交通', amount: 16 },
{ date: '2025-03-02', category: '餐饮', amount: 65.1 },
{ date: '2025-03-17', category: '教育', amount: 320 },
{ date: '2025-03-26', category: '医疗', amount: 180 },
{ date: '2025-03-28', category: '居住', amount: 2200 }
];
function parseMonth(s) {
const d = new Date(s);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
return `${y}-${m}`;
}
function sumByCategory(list) {
const map = new Map();
for (const b of list) map.set(b.category, (map.get(b.category) || 0) + b.amount);
return Array.from(map, ([category, total]) => ({ category, total }));
}
function sumByMonth(list) {
const map = new Map();
for (const b of list) {
const k = parseMonth(b.date);
map.set(k, (map.get(k) || 0) + b.amount);
}
return Array.from(map, ([month, total]) => ({ month, total })).sort((a, b) => a.month.localeCompare(b.month));
}
const pie = echarts.init(document.getElementById('pie'));
const line = echarts.init(document.getElementById('line'));
function renderAll(filteredBills) {
const catTotals = sumByCategory(filteredBills);
const pieOption = {
tooltip: {},
legend: { top: 'bottom' },
series: [
{ type: 'pie', radius: ['40%', '70%'], data: catTotals.map(o => ({ name: o.category, value: Number(o.total.toFixed(2)) })) }
]
};
const monthTotals = sumByMonth(filteredBills);
const lineOption = {
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: monthTotals.map(o => o.month) },
yAxis: { type: 'value' },
series: [
{ name: '月支出', type: 'line', smooth: true, showSymbol: false, data: monthTotals.map(o => Number(o.total.toFixed(2))) }
],
dataZoom: [{ type: 'inside' }, { type: 'slider' }]
};
pie.setOption(pieOption);
line.setOption(lineOption);
}
renderAll(bills);
document.getElementById('categoryFilter').addEventListener('change', function (e) {
const value = e.target.value;
const next = value === 'all' ? bills : bills.filter(b => b.category === value);
renderAll(next);
});
window.addEventListener('resize', function () {
pie.resize();
line.resize();
});
</script>
</body>
</html>
总结
- 数据结构化是基础,聚合策略决定统计的可靠性与性能
- ECharts 提供丰富图形与交互能力,覆盖占比与趋势两大核心需求
- 可视化不是终点,结合预算线、异常提醒与导出能力,才能形成闭环的家庭财务管理工具