归档页日历热力图实现

AI 智能总结

简介

归档页面新增了日历热力图功能,类似 GitHub 贡献图,直观展示过去一年的文章发布频率。每个格子代表一天,颜色越深表示当天发文越多,鼠标悬停可查看具体日期和发文数量。

技术选型

热力图选择 SVG 实现而非 Canvas,原因如下:

  • SVG 支持 CSS 变量,可以直接复用主题的暗色模式颜色系统
  • SVG 元素支持 DOM 事件(hover、click),无需手动计算坐标
  • 通过 viewBox + preserveAspectRatio 实现自适应缩放,无需 JavaScript 监听 resize
  • 文本标签(月份、星期)直接使用 SVG <text> 元素,渲染清晰

数据准备

Hugo 模板层

archives.html 模板中,遍历所有文章收集日期计数:

1{{ $dateCounts := dict }}
2{{ range $allPages }}
3  {{ $dateKey := .Date.Format "2006-01-02" }}
4  {{ $cur := index $dateCounts $dateKey | default 0 }}
5  {{ $dateCounts = merge $dateCounts (dict $dateKey (add $cur 1)) }}
6{{ end }}
7{{ $dateCountsJSON := $dateCounts | jsonify }}

将 JSON 数据注入 HTML 的 data-counts 属性:

1<div class="heatmap-grid" id="heatmap-grid" data-counts="{{ $dateCountsJSON }}"></div>

数据结构

生成的 JSON 格式为 "YYYY-MM-DD": 计数

1{
2  "2026-05-16": 34,
3  "2026-05-17": 3,
4  "2026-05-18": 15,
5  "2026-06-03": 7
6}

渲染逻辑

1. 计算日期范围

以当天为基准,前推 53 周(约一年),并对齐到周日开始:

1var today = new Date();
2var endDate = new Date(today);
3endDate.setDate(endDate.getDate() + (6 - endDate.getDay())); // 对齐到周六
4var startDate = new Date(endDate);
5startDate.setDate(startDate.getDate() - 52 * 7); // 前推 52 周

2. 构建网格数据

遍历 53 列 x 7 行,为每个单元格生成日期、计数和位置信息,同时记录每个月第一天的列位置用于月份标签:

 1for (var c = 0; c < 53; c++) {
 2  for (var r = 0; r < 7; r++) {
 3    var key = formatDate(d); // YYYY-MM-DD
 4    var cnt = counts[key] || 0;
 5    cells.push({col: c, row: r, date: d, count: cnt, key: key});
 6    // 记录月份标签
 7    if (d.getMonth() !== lastMonth && d.getDate() <= 7) {
 8      monthPositions.push({col: c, month: d.getMonth()});
 9      lastMonth = d.getMonth();
10    }
11    d.setDate(d.getDate() + 1);
12  }
13}

3. SVG 渲染

使用 viewBox 设置坐标系,preserveAspectRatio="xMidYMid meet" 实现等比缩放:

1var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2svg.setAttribute('viewBox', '0 0 ' + totalW + ' ' + totalH);
3svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
4svg.setAttribute('width', '100%');
5svg.setAttribute('height', '100%');

每个 <rect> 元素代表一天,填充颜色根据计数分级:

1var lv = 0;
2if (cell.count >= 4) lv = 4;
3else if (cell.count >= 3) lv = 3;
4else if (cell.count >= 2) lv = 2;
5else if (cell.count >= 1) lv = 1;
6rect.setAttribute('fill', getHeatColors()[lv]);

4. 颜色系统

支持亮色和暗色两套配色:

级别条件亮色暗色
lv00 篇#ebedf0#1e293b
lv11 篇#9be9a8#0e4429
lv22 篇#40c463#006d32
lv33 篇#30a14e#26a641
lv44+ 篇#216e39#39d353

通过检测 data-theme 属性动态切换:

1function getHeatColors() {
2  var isDark = document.documentElement.dataset.theme === 'dark';
3  return isDark
4    ? ['#1e293b', '#0e4429', '#006d32', '#26a641', '#39d353']
5    : ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'];
6}

5. 悬停提示

为每个 <rect> 绑定 mouseenter/mousemove/mouseleave 事件,显示浮动 tooltip:

 1rect.addEventListener('mouseenter', function(e) {
 2  tooltip.textContent = cell.key + ' · ' + cell.count + ' 篇';
 3  tooltip.style.display = 'block';
 4});
 5rect.addEventListener('mousemove', function(e) {
 6  var rect2 = grid.getBoundingClientRect();
 7  tooltip.style.left = (e.clientX - rect2.left + 12) + 'px';
 8  tooltip.style.top = (e.clientY - rect2.top - 32) + 'px';
 9});
10rect.addEventListener('mouseleave', function() {
11  tooltip.style.display = 'none';
12});

布局设计

热力图位于归档页面标题下方、文章列表上方,包含三个部分:

 1┌─────────────────────────────────────────────┐
 2│  📅 发文热力             少 ░ ▒ ▓ █ 多     │  ← 标题栏 + 图例
 3├─────────────────────────────────────────────┤
 4│     1月   2月   3月   ...   12月            │  ← 月份标签
 5│  日 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░     │
 6│  一 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░     │
 7│  二 ░░░░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░     │  ← 53周 x 7天
 8│  三 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░     │
 9│  ...                                        │
10│  六 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░     │
11└─────────────────────────────────────────────┘

自适应缩放

使用 SVG viewBox 原生缩放而非 CSS transform: scale()

  • transform: scale() 不改变元素布局空间,缩放后仍占用原始尺寸,导致溢出和滚动条
  • viewBox 缩放后元素实际占位就是缩放后的尺寸,容器自动适配
1.heatmap-grid {
2  width: 100%;
3  overflow: hidden;
4}
5.heatmap-grid svg {
6  width: 100%;
7  height: auto;
8}

文件结构

1layouts/
2├── _default/archives.html    # 归档模板(数据注入 + 渲染 JS)
3└── partials/
4    └── archive-post-list.html # 文章列表(不变)
5
6assets/
7└── css/pages.css              # 热力图样式 + 暗色主题

总结

日历热力图通过 Hugo 模板收集日期数据、SVG 渲染网格、viewBox 自适应缩放,实现了简洁高效的发布频率可视化。全程无外部依赖,纯原生 JavaScript + SVG,与主题暗色模式无缝集成。

留言评论

期待你的想法

评论加载中