前端可视化家庭账单:用 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 提供丰富图形与交互能力,覆盖占比与趋势两大核心需求
  • 可视化不是终点,结合预算线、异常提醒与导出能力,才能形成闭环的家庭财务管理工具
相关推荐
IT_陈寒1 小时前
Vue3性能优化实战:5个被低估的Composition API技巧让你的应用快30%
前端·人工智能·后端
嘻嘻哈哈猿人1 小时前
从 0 到 1 实现一个支持 @ 提及用户的输入框组件(Vue3 实战)
前端·vue.js
东土也1 小时前
Vue 项目 Nginx 部署路径差异分析与部署指南
前端
云枫晖1 小时前
Vue3 响应式原理:手写实现 ref 函数
前端·vue.js
合作小小程序员小小店2 小时前
web网页开发,在线%宠物销售%系统,基于Idea,html,css,jQuery,java,ssh,mysql。
java·前端·数据库·mysql·jdk·intellij-idea·宠物
荔枝吖2 小时前
html2canvas+pdfjs 打印html
前端·javascript·html
文心快码BaiduComate2 小时前
全运会,用文心快码做个微信小程序帮我找「观赛搭子」
前端·人工智能·微信小程序
合作小小程序员小小店2 小时前
web网页开发,在线%档案管理%系统,基于Idea,html,css,jQuery,java,ssh,mysql。
java·前端·mysql·jdk·html·ssh·intellij-idea
合作小小程序员小小店2 小时前
web网页开发,在线%物流配送管理%系统,基于Idea,html,css,jQuery,java,ssh,mysql。
java·前端·css·数据库·jdk·html·intellij-idea