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