iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Modern Web

Modern Web × AI《拖延怪日記》:語錄陪伴擺脫拖延系列 第 20

【Day 20】— 入門 JavaScript 網頁架設:長條圖統計

  • 分享至 

  • xImage
  •  

摘要
今天我們要把「精神狀態小測驗」升級成長期回顧工具!
不只保留最新一次的選項(MOOD_KEY),還要新增 MOOD_LOG_KEY 陣列,把每次提交的結果都存起來。這樣就能在歷史回顧頁顯示統計清單,甚至用 Chart.js 畫出長條圖,看到自己的精神狀態分布。

為什麼要做統計?

  • 從點到面:不只看「今天」;把每次提交都留下,回顧更有洞見。
  • 累積回饋:每次提交的小小紀錄,最後會變成完整的自我觀察。
  • 視覺化動力:圖表比文字清單更容易感受到變化。

學習重點

  • 陣列收集多筆答案:MOOD_LOG_KEY 陣列中,每次提交一筆 { ts, morning?, afternoon?, evening? }。
  • 動態聚合:把所有快照的 value 做計數,產出 { 狀態文字: 次數 } 的 Map。
  • 視覺化(Optional):若載入 Chart.js 成功,畫出長條圖;否則顯示回退清單。

核心流程

  1. 使用者在 Day 14 小測驗完成後,按下 submit → 觸發:
  • 把當前 MOOD_KEY 狀態轉成一個快照 { ts, morning, afternoon, evening }
  • 追加進 MOOD_LOG_KEY 陣列
  1. 在歷史回顧頁(History Section)新增一個 「精神狀態分布」 區塊:
  • 清單模式:顯示各個狀態出現幾次。
  • 圖表模式:用 Chart.js 畫長條圖。

實作

假設已具備:

  • Day 12
    showIntro()、hideIntroShowForm()、基本頁面骨架與主區塊切換。
  • Day 13
    表單提交流程 onSubmit()、motivationSection 顯示/隱藏、
    歷史清單:readRecords()、writeRecords()、renderHistory()、排序/篩選與偏好同步。
  • Day 14
    MOOD_KEY(當天最新狀態回填)、saveMood(period,value)、loadMood()、
    showFeedback()、showAggregateFeedback()、三時段單選 UI。
  • Day 15
    決策頁 moodDecisionSection、renderTodayRecap()、showPage('moodDecision')、
    互動按鈕 btnStartNow / btnLater_2。
  • Day 16
    任務安排:SCHEDULE_KEY、renderTaskCard()、placeTaskTo(period)、
    readScheduleMap()、writeScheduleMap()、showPage('taskSchedule')。
  • Day 17
    任務卡拖放(drag & drop)至三個時段格子。
  • Day 18
    溫暖總結 renderArrangementSummary();分享/查看歷史的動作按鈕。
  • Day 19
    回訪旗標 visited_before、showRevisitIntro()、覆寫後的 showIntro()、
    YES/NO 分支與四個行動按鈕導向。
  1. HTML 改動
    在歷史回顧頁新增統計區塊,並在 <canvas> 外包一層固定高度容器:
<!-- [Day20-NEW] 精神狀態分布(圖表或清單) -->
<div id="moodStatsSection" aria-labelledby="mood-stats-title" style="margin:.5rem 0;">
  <h2 id="mood-stats-title">精神狀態分布</h2>

  <!-- 固定高度的容器,避免圖表無限放大 -->
  <div id="moodChartWrap" style="position:relative; height:260px; max-width:680px;">
    <canvas id="moodChart" hidden aria-label="精神狀態長條圖" role="img"></canvas>
  </div>

  <!-- 回退清單 -->
  <ul id="moodStatsList" class="muted" aria-live="polite"></ul>
</div>
  1. JS 改動
    保留 MOOD_KEY,新增 MOOD_LOG_KEY 來收集快照,並在 History 頁面渲染統計:
// [Day20-NEW] 跨次提交的快照陣列
const MOOD_LOG_KEY = 'daily_mood_log';  
function readMoodLog() {
  try { return JSON.parse(localStorage.getItem(MOOD_LOG_KEY)) || []; }
  catch { return []; }
}
function writeMoodLog(arr) {
  localStorage.setItem(MOOD_LOG_KEY, JSON.stringify(Array.isArray(arr) ? arr : []));
}
function appendMoodSnapshot(snapshot) {
  const list = readMoodLog();
  list.push(snapshot);
  writeMoodLog(list);
}

// 由 MOOD_KEY 當前資料生成「一次提交快照」
function makeSnapshotFromCurrentMood() {
  const moodData = safeParseJSON(localStorage.getItem(MOOD_KEY), {});
  const snap = { ts: Date.now() };
  if (moodData.morning)   snap.morning   = moodData.morning;
  if (moodData.afternoon) snap.afternoon = moodData.afternoon;
  if (moodData.evening)   snap.evening   = moodData.evening;
  return snap;
}

// 聚合統計
function aggregateMoodCounts() {
  const log = readMoodLog();
  const counts = new Map();
  for (const s of log) {
    ['morning','afternoon','evening'].forEach(p => {
      const v = s[p];
      if (!v) return;
      counts.set(v, (counts.get(v) || 0) + 1);
    });
  }
  return counts;
}

// [Day20-NEW] 渲染統計(清單 + 圖表)
async function renderMoodStats() {
  const chartEl = document.getElementById('moodChart');
  const listEl  = document.getElementById('moodStatsList');
  if (!chartEl || !listEl) return;

  const counts = aggregateMoodCounts();

  // 清單模式
  if (counts.size === 0) {
    listEl.innerHTML = '<li>尚無統計資料(去做一次精神小測驗並按 submit 吧!)</li>';
  } else {
    const rows = [...counts.entries()].sort((a,b)=>b[1]-a[1]);
    listEl.innerHTML = rows.map(([k,v]) => `<li>${k}:${v} 次</li>`).join('');
  }

  // 載入 Chart.js
  try {
    if (!window.Chart) {
      await new Promise((resolve, reject) => {
        const s = document.createElement('script');
        s.src = 'https://cdn.jsdelivr.net/npm/chart.js';
        s.onload = resolve; s.onerror = reject;
        document.head.appendChild(s);
      });
    }
  } catch {
    chartEl.hidden = true;
    return;
  }

  if (counts.size === 0) {
    chartEl.hidden = true;
    return;
  }

  // 準備資料
  const rows = [...counts.entries()].sort((a,b)=>b[1]-a[1]);
  const labels = rows.map(([k]) => k);
  const data   = rows.map(([,v]) => v);

  chartEl.hidden = false;
  if (chartEl._chartInstance) chartEl._chartInstance.destroy();

  const ctx = chartEl.getContext('2d');
  chartEl._chartInstance = new Chart(ctx, {
    type: 'bar',
    data: { labels, datasets: [{ label: '出現次數', data }] },
    options: {
      responsive: true,
      maintainAspectRatio: false, // 交給容器高度控制
      plugins: { legend: { display: false } },
      scales: { y: { beginAtZero: true, precision: 0 } }
    }
  });
}

在 showPage('history') 時呼叫:

if (page === 'history') {
  historySection.hidden = false;
  renderMoodStats().finally(() => {
    if (typeof renderHistory === 'function') renderHistory();
  });
}

驗證

  1. 進行精神小測驗 → 勾選三時段 → 按 submit。
  2. 查看歷史回顧頁,會看到:
  • 清單:每種狀態的次數
  • 圖表:固定高度的長條圖,不會再無限放大
  1. 多次提交後,分布圖會隨著紀錄累積而更新。

常見錯誤 & 排查

  1. 沒有任何統計
  • 確認是否有按下小測驗的 submit(moodSubmitBtn)。
  • 檢查 daily_mood_log 是否為合法 JSON 陣列。若破損,清掉再重新測試。
  1. 圖表不出現
  • 開發者工具看 Network:CDN 是否被阻擋?若是,屬預期降級,會顯示統計清單。
  1. 清單排序和數字看起來怪
  • 我們以所有快照集中計數,同一日多次提交會全部累計(符合「每次提交都算」的規格)。
  1. 快照記錄是空白
  • 當三時段皆未選時,會先詢問是否提交空白快照;若選「是」,將只存 ts。統計自然不會增加數字。

上一篇
【Day 19】— 入門 JavaScript 網頁架設:狀態追蹤(再次造訪的互動)
下一篇
【Day 21】— 入門 JavaScript 網頁架設:圓餅/趨勢圖(Chart.js)
系列文
Modern Web × AI《拖延怪日記》:語錄陪伴擺脫拖延21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言