前端可视化家庭账单:用 ECharts 实现支出统计与趋势分析

前端可视化家庭账单:用 ECharts 实现支出统计与趋势分析

在家庭财务管理中,直观地看懂钱花到了哪里、花得是否稳定,是提高消费意识与优化预算的关键。本文以 ECharts 为核心,构建一个可视化的家庭账单分析:包括支出分类统计、月度趋势分析、交互筛选与性能优化建议,帮助你在浏览器端快速落地一个实用的可视化面板。

适用场景

  • 需要按类别统计支出占比并快速定位高频支出项
  • 需要观察月度支出变化趋势并识别异常波动
  • 希望在不引入后端的前提下,完成本地或前端的数据分析与展示

数据模型设计

为后续统计与可视化,建议将每笔账单设计为结构化数据:

json 复制代码
[
  {
    "date": "2025-01-03",
    "category": "餐饮",
    "amount": 56.5,
    "paymentMethod": "信用卡",
    "note": "外卖"
  }
]

关键字段说明:

  • dateYYYY-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 聚合求和,并按需排序
  • 饼图适合看比例结构,若类别较多可切换为水平条形图以增强可读性
  • 可配合 legendselected 实现类别筛选

趋势分析:月度变化

  • 依据 date 转换成 YYYY-MM 进行月度聚合
  • 折线图的 smooth 能提升趋势观感,搭配 areaStyle 强化视觉层次
  • 可在异常峰值处使用 markPointvisualMap 进行突出标记

交互增强

  • 时间维度筛选:按年、按月或自定义区间筛选并重新渲染
  • 类别筛选:使用图例勾选或下拉框控制类别数据是否参与统计
  • 多图联动:点击饼图某分类时,联动折线图仅展示该分类在各月的趋势

性能与数据质量

  • 数据量较大时,尽量在聚合前做去噪与无效记录过滤
  • 前端聚合建议使用原生结构与一次遍历完成,避免多次 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 提供丰富图形与交互能力,覆盖占比与趋势两大核心需求
  • 可视化不是终点,结合预算线、异常提醒与导出能力,才能形成闭环的家庭财务管理工具
相关推荐
沙蒿同学5 分钟前
我把架构约定编译成了会变红的测试:Wails v2 + Go + Vue3 桌面脚手架实战
前端·后端·github
cpolar技术支持8 分钟前
本地 Playwright 测试报告怎么远程复盘?Trace Viewer 跑起来后,用 cpolar 分享失败现场
前端·自动化测试·测试工具·cpolar·playwright
wordbaby8 分钟前
企业级后台管理系统路由设计与最佳实践指南
前端
胡志辉的博客18 分钟前
【完全开源】IP 纯净度检测 可一键部署到自己的CF
前端·javascript·chrome·ip·chromium
Hilaku44 分钟前
作为面试官,我最怕遇到什么样的候选人?
前端·javascript·程序员
TiDi1 小时前
Pinia优化重复请求
前端
web3d5201 小时前
01-用 Leafletjs 10 分钟搭一张水利一张图(Vue3 + Vite 实战)
前端·javascript
晚安日记wanna1 小时前
Vue2 的 defineProperty 差在哪四层追问筛掉九成候选人
前端·vue.js·面试
TiDi2 小时前
吸顶导航交互实现
前端
kisshyshy2 小时前
从Props透传到自定义Hook:系统梳理React跨层级通信与逻辑复用
前端·架构·代码规范