เทคนิคที่ 025 · PYTHON / JAVASCRIPT

ปฏิทินความร้อนCalendar Heatmap

เห็นวันหยุด ฤดูกาล และช่วงที่หายไปในภาพเดียว

3
ผสานหลายแนวคิดควรรู้ก่อน · เข้าใจว่าสีเข้มขึ้นแทนค่าที่มากขึ้น
ลองเล่น

01ภาพรวมของกราฟนี้

Calendar heatmap วางแต่ละวันเป็นช่องในตารางที่คอลัมน์คือสัปดาห์และแถวคือวันในสัปดาห์ การจัดแบบนี้ทำให้รูปแบบสองชั้นปรากฏพร้อมกัน คือรูปแบบรายสัปดาห์ในแนวตั้งและรูปแบบรายฤดูกาลในแนวนอน

เปรียบเทียบให้เห็นภาพ

เหมือนปฏิทินตั้งโต๊ะที่เราระบายสีช่องตามปริมาณงานของแต่ละวัน พอมองทั้งแผ่นก็เห็นทันทีว่าช่วงไหนยุ่งและวันไหนของสัปดาห์ที่ว่างเสมอ

ใช้ที่ไหนบ้าง — จำนวนคำสั่งซื้อรายวัน กิจกรรมของผู้ใช้ ปริมาณการใช้ไฟฟ้า หรือการติดตามพฤติกรรมรายวันตลอดปี

สร้างอย่างไร

  1. ขั้นที่ 1จัดวันเป็นตารางสัปดาห์ × วันในสัปดาห์
  2. ขั้นที่ 2เลือกช่วงสีแบบเฉดเดียว
  3. ขั้นที่ 3ระบายสีตามค่า
  4. ขั้นที่ 4ใส่ legend และทำเครื่องหมายวันหยุด

ตัวอย่างคลาสสิก · GitHub contribution graph

ตารางสีเขียวที่แสดงกิจกรรมรายวันของนักพัฒนาตลอดปี เป็นตัวอย่างที่ทำให้ปฏิทินความร้อนกลายเป็นรูปแบบที่คนทั่วไปคุ้นเคย และแสดงจุดแข็งของมันชัดเจน คือเห็นทั้งวันที่ขยันและช่วงที่หายไปในภาพเดียว

GitHub Docs · Viewing contributions

02อธิบายภาษาคนเข้าใจง่าย ๆ

ข้อมูลรายวันตลอดปีมี 365 จุด ถ้าวาดเป็นกราฟเส้นจะได้เส้นหยักถี่ยิบที่บอกภาพรวมไม่ได้ และมองไม่เห็นเลยว่าวันเสาร์อาทิตย์ต่างจากวันธรรมดาไหม

ปฏิทินความร้อนแก้ด้วยการเปลี่ยนแกนเวลาหนึ่งมิติให้เป็นตารางสองมิติ คอลัมน์คือสัปดาห์ที่ไล่จากซ้ายไปขวา และแถวคือวันจันทร์ถึงอาทิตย์

ทันทีที่จัดแบบนี้ รูปแบบสองชั้นก็โผล่ออกมาเอง ถ้าแถวล่างสองแถวจางกว่าเสมอแปลว่าเสาร์อาทิตย์เงียบ และถ้าคอลัมน์ช่วงกลางเข้มกว่าแปลว่ามีฤดูกาล

สิ่งที่กราฟนี้เก่งเป็นพิเศษคือการหาช่องว่าง ถ้ามีแถบจางยาว ๆ กลางปี นั่นอาจเป็นช่วงที่ระบบเก็บข้อมูลล่ม ซึ่งเป็นสิ่งที่กราฟเส้นมักกลบไป

ข้อจำกัดคืออ่านค่าที่แม่นยำไม่ได้เพราะเข้ารหัสด้วยความเข้มของสี ควรใช้คู่กับ tooltip หรือมีกราฟเส้นข้าง ๆ สำหรับการอ่านค่า

03ข้อมูลที่ต้องใช้

รูปทรงของข้อมูลอนุกรมเวลา
จำนวนมิติที่เข้ารหัสได้3 มิติ
จำนวนแถวที่เหมาะ365 วัน (หนึ่งปีเต็ม)ขั้นต่ำ 60 แถว
เส้นฐานศูนย์ไม่เกี่ยวข้องไม่มีแกนค่า ค่าถูกเข้ารหัสด้วยความเข้มของสี

คอลัมน์ที่ต้องเตรียม

คอลัมน์ทำหน้าที่ชนิดเข้ารหัสด้วยจำนวนค่าที่เหมาะหมายเหตุ
dateวันที่เวลาx90–730 วันต้องเป็นรายวันและควรครอบคลุมทั้งช่วงโดยไม่ข้ามวัน
valueค่าของวันนั้นตัวเลขต่อเนื่องcolorต่อเนื่องหรือจำนวนนับเข้ารหัสด้วยความเข้มของสี จึงอ่านค่าได้หยาบ

ต้องเตรียมข้อมูลอย่างไรก่อนวาด

  1. เติมวันที่ขาดให้ครบทุกวันในช่วง โดยทำเครื่องหมายว่าไม่มีข้อมูล
  2. ตัดสินใจว่าจะใช้ช่วงสีแบบเชิงเส้นหรือแบบควอนไทล์
  3. เตรียมรายการวันหยุดไว้ทำเครื่องหมาย

ค่าที่หายไป — ★ วันที่ไม่มีข้อมูลต้องใช้สีที่ต่างจากศูนย์อย่างชัดเจน เช่น สีเทาลายทาง เพราะ "ไม่มีคำสั่งซื้อ" กับ "ไม่ได้เก็บข้อมูล" เป็นคนละเรื่อง

สเกลของแกน — เฉดเดียวอ่อนไปเข้ม ถ้าข้อมูลเบ้แรงให้ใช้การแบ่งแบบควอนไทล์แทนเชิงเส้น แต่ต้องเขียนบอก

กราฟจะเริ่มพังเมื่อไร

เกิน 2 ปีในภาพเดียวช่องจะเล็กจนกดไม่ได้ — แยกเป็นปีละแถว

หน้าตาไฟล์ที่ต้องเตรียม

date,value

04อ่านแล้วเห็นอะไร

สิ่งที่เห็นได้จากภาพ

  • รูปแบบรายสัปดาห์ เช่น เสาร์อาทิตย์ต่างจากวันธรรมดา
  • รูปแบบตามฤดูกาลจากความเข้มที่เปลี่ยนไปตามคอลัมน์
  • วันที่โดดเด่นผิดปกติ
  • ช่วงที่ข้อมูลหายไป

สิ่งที่กราฟนี้ตอบไม่ได้

  • อ่านค่าที่แม่นยำไม่ได้เพราะใช้ความเข้มของสี
  • ไม่บอกแนวโน้มระยะยาวได้ชัดเท่ากราฟเส้น

อ่านตามลำดับนี้

  1. มองแนวตั้งก่อนเพื่อหารูปแบบรายสัปดาห์
  2. มองแนวนอนเพื่อหารูปแบบตามฤดูกาล
  3. หาช่องหรือแถบที่ผิดจากรอบข้าง

คำถามที่ตอบได้

  • วันไหนของสัปดาห์ที่ยุ่งที่สุด
  • มีฤดูกาลหรือไม่
  • มีช่วงที่ข้อมูลขาดหายไปไหม

สัญญาณว่ามีอะไรผิด

  • ใช้สีเดียวกันสำหรับศูนย์กับไม่มีข้อมูล
  • ใช้ rainbow palette ซึ่งทำให้ลำดับของค่าอ่านไม่ออก

05เหมาะกับการนำเสนอแบบไหน

ผู้ชมที่เหมาะทุกคน รูปแบบปฏิทินเป็นสิ่งที่คนคุ้นเคยอยู่แล้ว
เวลาที่ผู้ชมต้องใช้ทันที
ขนาดเล็กสุดที่ยังอ่านได้560×160 px
dashboard ที่อัปเดตตลอด
รายงานเชิงวิเคราะห์
สื่อสารกับคนทั่วไป
สิ่งพิมพ์
มือถือช่องเล็กเกินกว่าจะกดบนจอสัมผัส

ทำอย่างไรให้คนอ่านได้ข้อสรุปโดยไม่ต้องบรรยาย

  1. ใส่ legend ไล่เฉดพร้อมค่าต่ำสุดและสูงสุด
  2. ทำเครื่องหมายวันหยุดสำคัญ
  3. ใช้สีที่ต่างชัดเจนสำหรับวันที่ไม่มีข้อมูล

รูปแบบชื่อกราฟที่ควรใช้

กิจกรรมสูงสุดใน <ช่วงเวลา> โดย <วันในสัปดาห์> เงียบที่สุดอย่างสม่ำเสมอ

06เอาไปใช้ต่อ

โค้ดทุกชิ้นอ่านไฟล์ calendar-heatmap-sample.csv ชุดเดียวกับที่ Playground ใช้ และตัวเลขในโค้ดเปลี่ยนตามค่าที่คุณปรับด้านล่าง

# ติดตั้ง: pip install "matplotlib==3.10.*" "pandas==2.*"
# ดาวน์โหลด calendar-heatmap-sample.csv จากหน้านี้ แล้ววางไว้ข้างสคริปต์
import matplotlib
matplotlib.use("Agg")   # รันแบบไม่มีหน้าจอ ถ้ารันในเครื่องตัวเองลบบรรทัดนี้ได้
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

C1, C2, C3 = "#2a78d6", "#eb6834", "#1baf7a"   # palette ของคลัง เปลี่ยนเป็นสีองค์กรได้
INK, SURFACE = "#0b0b0b", "#fcfcfb"

df = pd.read_csv("calendar-heatmap-sample.csv")
df["value"] = pd.to_numeric(df["value"], errors="coerce")
NOTE = "weekly"   # ← ค่าจาก Playground

fig, ax = plt.subplots(figsize=(7.6, 4.2), facecolor=SURFACE)
ax.set_facecolor(SURFACE)

df["date"] = pd.to_datetime(df["date"])
d = df.sort_values("date")
d["week"] = ((d["date"] - d["date"].min()).dt.days + d["date"].min().weekday()) // 7
d["dow"] = d["date"].dt.weekday
grid = np.full((7, d["week"].max() + 1), np.nan)   # nan = ไม่มีข้อมูล ต่างจากศูนย์
for _, r in d.iterrows():
    grid[int(r["dow"]), int(r["week"])] = r["value"]
im = ax.imshow(grid, cmap="Blues", aspect="equal")
ax.set_yticks(range(7)); ax.set_yticklabels(["จ", "อ", "พ", "พฤ", "ศ", "ส", "อา"], fontsize=8)
ax.set_xticks([]); fig.colorbar(im, ax=ax, shrink=0.6, label="ค่า")

ax.spines[["top", "right"]].set_visible(False)   # ลดเส้นที่ไม่ได้ให้ข้อมูล
fig.tight_layout()
fig.savefig("calendar-heatmap.png", dpi=160)
print("เขียน calendar-heatmap.png แล้ว ·", len(df), "แถว")

ติดตั้ง: pip install "matplotlib==3.10.*" "pandas==2.*" · ทดสอบกับเวอร์ชัน 3.10

07ลองเล่นเพื่อเข้าใจ

ข้อมูลตัวอย่าง · 180 แถว · seed 61● คำนวณใหม่ในเบราว์เซอร์ทันทีที่ปรับค่า
ดูแถวล่างสองแถวเทียบกับแถวบน
อา1/1/2568 · 24.02/1/2568 · 19.03/1/2568 · 24.04/1/2568 · 4.005/1/2568 · 9.006/1/2568 · 19.07/1/2568 · 28.08/1/2568 · 25.09/1/2568 · 16.010/1/2568 · 28.011/1/2568 · 3.0012/1/2568 · 6.0013/1/2568 · 27.014/1/2568 · 25.015/1/2568 · 32.016/1/2568 · 18.017/1/2568 · 21.018/1/2568 · 7.0019/1/2568 · 5.0020/1/2568 · 32.021/1/2568 · 24.022/1/2568 · 23.023/1/2568 · 25.024/1/2568 · 20.025/1/2568 · 10.026/1/2568 · 5.0027/1/2568 · 18.028/1/2568 · 19.029/1/2568 · 21.030/1/2568 · 23.031/1/2568 · 24.01/2/2568 · 6.002/2/2568 · 7.003/2/2568 · 23.04/2/2568 · 24.05/2/2568 · 23.06/2/2568 · 32.07/2/2568 · 25.08/2/2568 · 6.009/2/2568 · 8.0010/2/2568 · 22.011/2/2568 · 21.012/2/2568 · 25.013/2/2568 · 13.014/2/2568 · 16.015/2/2568 · 7.0016/2/2568 · 5.0017/2/2568 · 33.018/2/2568 · 25.019/2/2568 · 24.020/2/2568 · 22.021/2/2568 · 31.022/2/2568 · 7.0023/2/2568 · 9.0024/2/2568 · 28.025/2/2568 · 23.026/2/2568 · 15.027/2/2568 · 24.028/2/2568 · 18.01/3/2568 · 8.002/3/2568 · 3.003/3/2568 · 23.04/3/2568 · 26.05/3/2568 · 19.06/3/2568 · 32.07/3/2568 · 31.08/3/2568 · 9.009/3/2568 · 5.0010/3/2568 · 14.011/3/2568 · 28.012/3/2568 · 13.013/3/2568 · 25.014/3/2568 · 23.015/3/2568 · 4.0016/3/2568 · 6.0017/3/2568 · 33.018/3/2568 · 22.019/3/2568 · 33.020/3/2568 · 14.021/3/2568 · 12.022/3/2568 · 6.0023/3/2568 · 5.0024/3/2568 · 24.025/3/2568 · 25.026/3/2568 · 25.027/3/2568 · 25.028/3/2568 · 22.029/3/2568 · 6.0030/3/2568 · 5.0031/3/2568 · 14.01/4/2568 · 25.02/4/2568 · 17.03/4/2568 · 19.04/4/2568 · 19.05/4/2568 · 8.006/4/2568 · 4.007/4/2568 · 21.08/4/2568 · 25.09/4/2568 · 16.010/4/2568 · 23.011/4/2568 · 21.012/4/2568 · 5.0013/4/2568 · 10.014/4/2568 · 20.015/4/2568 · 23.016/4/2568 · 18.017/4/2568 · 27.018/4/2568 · 30.019/4/2568 · 11.020/4/2568 · 10.021/4/2568 · 16.022/4/2568 · 24.023/4/2568 · 25.024/4/2568 · 25.025/4/2568 · 26.026/4/2568 · 11.027/4/2568 · 6.0028/4/2568 · 24.029/4/2568 · 19.030/4/2568 · 21.01/5/2568 · 28.02/5/2568 · 19.03/5/2568 · 6.004/5/2568 · 3.005/5/2568 · 18.06/5/2568 · 18.07/5/2568 · 21.08/5/2568 · 15.09/5/2568 · 16.010/5/2568 · 9.0011/5/2568 · 7.0012/5/2568 · 14.013/5/2568 · 18.014/5/2568 · 31.015/5/2568 · 23.016/5/2568 · 9.0017/5/2568 · 8.0018/5/2568 · 6.0019/5/2568 · 17.020/5/2568 · 32.021/5/2568 · 18.022/5/2568 · 25.023/5/2568 · 21.024/5/2568 · 6.0025/5/2568 · 7.0026/5/2568 · 23.027/5/2568 · 34.028/5/2568 · 28.029/5/2568 · 17.030/5/2568 · 20.031/5/2568 · 6.001/6/2568 · 9.002/6/2568 · 22.03/6/2568 · 19.04/6/2568 · 18.05/6/2568 · 24.06/6/2568 · 28.07/6/2568 · 2.008/6/2568 · 4.009/6/2568 · 30.010/6/2568 · 29.011/6/2568 · 18.012/6/2568 · 22.013/6/2568 · 17.014/6/2568 · 9.0015/6/2568 · 5.0016/6/2568 · 27.017/6/2568 · 23.018/6/2568 · 33.019/6/2568 · 24.020/6/2568 · 23.021/6/2568 · 3.0022/6/2568 · 6.0023/6/2568 · 20.024/6/2568 · 24.025/6/2568 · 16.026/6/2568 · 13.027/6/2568 · 20.028/6/2568 · 2.0029/6/2568 · 6.00น้อยมาก
ค่าสูงสุด34.0
ค่าเฉลี่ยรายวัน17.9
วันที่เป็นศูนย์0
ดูข้อมูลเป็นตาราง (26 แถว)
ปฏิทินความร้อน 180 วัน ค่าสูงสุด 34.0
วันที่ค่า
2025-01-0124.0
2025-01-0825.0
2025-01-1532.0
2025-01-2223.0
2025-01-2921.0
2025-02-0523.0
2025-02-1225.0
2025-02-1924.0
2025-02-2615.0
2025-03-0519.0
2025-03-1213.0
2025-03-1933.0
2025-03-2625.0
2025-04-0217.0
2025-04-0916.0
2025-04-1618.0
2025-04-2325.0
2025-04-3021.0
2025-05-0721.0
2025-05-1431.0
2025-05-2118.0
2025-05-2828.0
2025-06-0418.0
2025-06-1118.0
2025-06-1833.0
2025-06-2516.0

ลองแล้วจะเห็นอะไร

  • สังเกตอะไร: มองแนวตั้งจะเห็นว่าสองแถวล่าง (เสาร์อาทิตย์) จางกว่าอย่างสม่ำเสมอ ซึ่งเป็นรูปแบบที่กราฟเส้นรายวันกลบไปหมด

ข้อควรระวัง

  1. ★ ใช้สีเดียวกันสำหรับ "ค่าเป็นศูนย์" กับ "ไม่มีข้อมูล" ซึ่งเป็นคนละความหมายโดยสิ้นเชิง
  2. ใช้ rainbow palette ทำให้ลำดับของค่าอ่านไม่ออก
  3. ใส่หลายปีในภาพเดียวจนช่องเล็กเกินกด
  4. ใช้ช่วงสีเชิงเส้นกับข้อมูลที่เบ้แรง ทำให้เกือบทุกช่องดูจางเหมือนกันหมด

เมื่อไหร่ไม่ควรใช้กราฟนี้

  • ข้อมูลไม่ใช่รายวันใช้แทน: heatmap-matrix หรือ line-chart
  • ต้องอ่านค่าที่แม่นยำใช้แทน: line-chart หรือ data-table-viz
  • มีข้อมูลน้อยกว่า 2 เดือนใช้แทน: bar-chart รายวัน

กราฟที่เกี่ยวข้อง

เทคนิคถัดไป · กราฟวงรอบ