前端可視化家庭賬單:用 ECharts 實現支出統計與趨勢分析

在家庭財務管理中,直觀地看懂錢花到了哪裏、花得是否穩定,是提高消費意識與優化預算的關鍵。本文以 ECharts 為核心,構建一個可視化的家庭賬單分析:包括支出分類統計、月度趨勢分析、交互篩選與性能優化建議,幫助你在瀏覽器端快速落地一個實用的可視化面板。

適用場景

  • 需要按類別統計支出佔比並快速定位高頻支出項
  • 需要觀察月度支出變化趨勢並識別異常波動
  • 希望在不引入後端的前提下,完成本地或前端的數據分析與展示

數據模型設計

為後續統計與可視化,建議將每筆賬單設計為結構化數據:

[
  {
    "date": "2025-01-03",
    "category": "餐飲",
    "amount": 56.5,
    "paymentMethod": "信用卡",
    "note": "外賣"
  }
]

關鍵字段説明:

  • dateYYYY-MM-DD 字符串,便於按月聚合
  • category:分類名稱,例如餐飲、交通、居住、教育、醫療、娛樂等
  • amount:支出金額,統一為正數
  • paymentMethod:支付方式,按需篩選或做子維度統計

基礎搭建

選擇純前端頁面即可運行,使用 CDN 引入 ECharts:

<!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,便於長期歸檔

完整示例(含類別聯動)

<!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 提供豐富圖形與交互能力,覆蓋佔比與趨勢兩大核心需求
  • 可視化不是終點,結合預算線、異常提醒與導出能力,才能形成閉環的家庭財務管理工具