iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Build on Google AI

《30天打造喵語日誌:Gemini API × Vibe Coding 實戰》系列 第 11

Day 11:【圖表模組】情緒歷程視覺化:用 Chart.js 畫出情緒起伏月曆

  • 分享至 

  • xImage
  •  

讓文字變成有溫度的數據歷程

在前幾篇文章中,我們完成了《喵語日誌》的文字對話、情緒標記和照片分享模組。

隨著使用者天天寫日記、紀錄心情,這些散落在對話視窗裡的資料會越來越多。如果只靠文字回顧,很難對自己的心理狀態有巨觀的理解。

例如:

  • 這週整體心情是偏開心還是偏低落?

  • 最近有沒有特別容易低潮的日子?

  • 哪些活動或情境常伴隨著好心情?

今天我們的目標,就是引入 Chart.js 圖表庫,將這些日常紀錄的心情點滴,轉化為清晰美麗的情緒起伏走勢圖

透過視覺化的折線圖,讓使用者能一眼看懂自己這週或這個月的心情變化,發覺那些低潮與高光的軌跡。

技術選型與套件安裝

在前端圖表庫的選擇上,我選用了生態系成熟且支援度極佳的 Chart.js,以及它的 React 封裝庫 react-chartjs-2

1. 安裝套件

在終端機中執行:

npm install chart.js react-chartjs-2

如果使用 React 19 遇到 peer dependencies 警告,可以加上 --legacy-peer-deps 參數:

npm install chart.js react-chartjs-2 --legacy-peer-deps

2. Chart.js 模組註冊

Chart.js v3+ 採用模組化設計,使用前必須在元件中註冊需要的元件,避免未使用的模組塞爆打包檔。

註冊的元件包含:

  • CategoryScale:X 軸使用的分類刻度。

  • LinearScale:Y 軸使用的線性刻度。

  • PointElementLineElement:折線圖的點和線。

  • TitleTooltipLegend:標題、提示框和圖例。

  • Filler:用於填充曲線下方區域。

核心邏輯:將抽象情緒轉化為可視化數值

要畫出折線圖,首先需要把抽象的情緒轉化為 Y 軸的數值分數。

我定義了一個簡單的對應關係:
https://ithelp.ithome.com.tw/upload/images/20260811/20178708MCARAXSUIN.png
這樣就可以把每天的情緒紀錄,轉換成 1~5 的數值,作為折線圖的 Y 軸數據。

MoodChart 元件實作

為了保持《喵語日誌》一貫的療癒風格,我對圖表做了一些視覺調整:

  • 平滑曲線: 設定 tension: 0.4,讓折線呈現自然的圓弧狀。

  • 暖色漸層: 使用粉紅色系填充曲線下方區域。

  • Y 軸標籤: 透過 ticks.callback 將數字轉換為貓咪表情,不再只是冰冷的數字。

src/components/MoodChart.tsx 完整實作如下::

import { Line } from "react-chartjs-2";
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  Title,
  Tooltip,
  Legend,
  Filler,
  type ChartData,
  type ChartOptions,
} from "chart.js";

// 1. 模組註冊
ChartJS.register(
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  Title,
  Tooltip,
  Legend,
  Filler
);

// 2. 抽取 Emoji 字典,避免重複宣告
const MOOD_EMOJIS: Record<number, string> = {
  1: "🙀",
  2: "😿",
  3: "😐",
  4: "😺",
  5: "😻",
};

export interface MoodLog {
  date: string;
  mood: string;
  score: number;
}

interface MoodChartProps {
  moodLogs: MoodLog[];
  period: "week" | "month";
}

export default function MoodChart({ moodLogs }: MoodChartProps) {
  const labels = moodLogs.map((log) => log.date);
  const scores = moodLogs.map((log) => log.score);

  // 折線圖資料與樣式設定
  const data: ChartData<"line"> = {
    labels,
    datasets: [
      {
        label: "心情分數",
        data: scores,
        borderColor: "#f43f5e",
        backgroundColor: "rgba(244, 63, 94, 0.15)",
        borderWidth: 3,
        fill: true,
        tension: 0.4,
        pointRadius: 5,
        pointBackgroundColor: "#f43f5e",
        pointBorderColor: "#ffffff",
        pointBorderWidth: 2,
      },
    ],
  };

  // 圖表客製化選項
  const options: ChartOptions<"line"> = {
    responsive: true,
    maintainAspectRatio: false,
    plugins: {
      legend: { display: false },
      tooltip: {
        callbacks: {
          label: (context) => {
            const val = context.raw as number;
            return `心情:${MOOD_EMOJIS[val] || val}`;
          },
        },
      },
    },
    scales: {
      y: {
        min: 1,
        max: 5,
        ticks: {
          stepSize: 1,
          callback: (value) => MOOD_EMOJIS[value as number] || "",
        },
      },
      x: {
        grid: { display: false },
      },
    },
  };

  return (
    <div className="h-64 w-full">
      <Line data={data} options={options} />
    </div>
  );
}

整合底部導覽列與週/月切換

為了讓操作體驗更像原生手機 App,我把原本的上下堆疊結構,改成透過底部導覽列進行分頁切換,並在圖表上方加入本週/本月的時間區間切換按鈕。

src/App.tsx 中整合頁籤與狀態控制:

import { useState } from "react";
import ChatContainer from "./components/ChatContainer";
import MoodChart, { MoodLog } from "./components/MoodChart";

type Tab = "chat" | "chart";

export default function App() {
  const [activeTab, setActiveTab] = useState<Tab>("chat");
  const [period, setPeriod] = useState<"week" | "month">("week");

  // 模擬心情紀錄資料
  const moodLogs: MoodLog[] = [
    { date: "8/5", mood: "Good", score: 4 },
    { date: "8/6", mood: "Great", score: 5 },
    { date: "8/7", mood: "Okay", score: 3 },
    { date: "8/8", mood: "Low", score: 2 },
    { date: "8/9", mood: "Good", score: 4 },
    { date: "8/10", mood: "Great", score: 5 },
    { date: "8/11", mood: "Good", score: 4 },
  ];

  return (
    <main className="mx-auto flex min-h-screen w-full max-w-md flex-col bg-stone-50">
      {/* 主要視窗區塊 */}
      <div className="flex-1 p-4">
        {activeTab === "chat" ? (
          <ChatContainer />
        ) : (
          <div>
            {/* 週/月切換標籤 */}
            <div className="mb-4 flex items-center justify-between">
              <h2 className="text-lg font-semibold text-stone-800">情緒走勢</h2>
              <div className="flex gap-2">
                <button
                  onClick={() => setPeriod("week")}
                  className={`rounded-full px-3 py-1 text-xs transition ${
                    period === "week"
                      ? "bg-rose-500 text-white"
                      : "bg-stone-200 text-stone-600"
                  }`}
                >
                  本週
                </button>
                <button
                  onClick={() => setPeriod("month")}
                  className={`rounded-full px-3 py-1 text-xs transition ${
                    period === "month"
                      ? "bg-rose-500 text-white"
                      : "bg-stone-200 text-stone-600"
                  }`}
                >
                  本月
                </button>
              </div>
            </div>

            {/* 圖表元件 */}
            <MoodChart moodLogs={moodLogs} period={period} />
          </div>
        )}
      </div>

      {/* 底部導覽列 */}
      <nav className="flex h-16 w-full items-center justify-around border-t border-rose-100/60 bg-white/90">
        <button
          onClick={() => setActiveTab("chat")}
          className={`text-sm font-medium transition ${
            activeTab === "chat" ? "text-rose-500" : "text-stone-400"
          }`}
        >
          喵喵對話
        </button>

        <button
          onClick={() => setActiveTab("chart")}
          className={`text-sm font-medium transition ${
            activeTab === "chart" ? "text-rose-500" : "text-stone-400"
          }`}
        >
          情緒分析
        </button>
      </nav>
    </main>
  );
}

實測成果展示

當使用者點擊底部導覽列的「情緒分析」按鈕時,主視窗會順暢切換到情緒圖表。

點擊右上角的「本週/本月」按鈕,曲線會即時更新。游標移到點位上時,Tooltip 也會跳出對應的心情標籤。

https://ithelp.ithome.com.tw/upload/images/20260811/20178708C6zlpXEGw9.png

整體畫面圓潤且富有質感,符合《喵語日誌》的療癒風格。

心得與結語

這次實作讓我深刻體會到,資料視覺化不僅是把數據畫出來,更重要的是選對呈現方式與視覺風格。從安裝並註冊 Chart.js 核心模組、將抽象情緒轉換為 1~5 分的數值,到自訂 Emoji 刻度與暖色平滑曲線,原本硬邦邦的圖表也能變得像繪本一樣溫暖。而整合底部導覽列與週月切換功能後,《喵語日誌》的模組切換變得更具體系感,為後續功能的擴充奠定了良好的 UI 骨架。

明天我們將進入全新的主題!我們將為《喵語日誌》引入遊戲化機制,實作「連續紀錄 3 天」、「上傳美食照」等成就卡片元件與徽章牆 UI,透過成就解鎖的回饋,提升使用者的紀錄動力與黏著度!喵~


上一篇
Day 10:【多媒體元件】前端照片上傳與預覽模組實作
下一篇
Day 12:【成就系統】遊戲化設計:成就解鎖卡片與徽章牆 UI
系列文
《30天打造喵語日誌:Gemini API × Vibe Coding 實戰》17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言