前端可视化家庭账单:用 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 提供丰富图形与交互能力,覆盖占比与趋势两大核心需求
  • 可视化不是终点,结合预算线、异常提醒与导出能力,才能形成闭环的家庭财务管理工具
相关推荐
EnCi Zheng12 分钟前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen16 分钟前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技16 分钟前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人27 分钟前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实28 分钟前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha39 分钟前
三目运算符
linux·服务器·前端
晓晨的博客1 小时前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding1 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
不可能的是1 小时前
从 /simplify 指令深挖 Claude Code 多 Agent 协同机制
javascript