简介
归档页面新增了日历热力图功能,类似 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. 颜色系统
支持亮色和暗色两套配色:
| 级别 | 条件 | 亮色 | 暗色 |
|---|---|---|---|
| lv0 | 0 篇 | #ebedf0 | #1e293b |
| lv1 | 1 篇 | #9be9a8 | #0e4429 |
| lv2 | 2 篇 | #40c463 | #006d32 |
| lv3 | 3 篇 | #30a14e | #26a641 |
| lv4 | 4+ 篇 | #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,与主题暗色模式无缝集成。
留言评论
期待你的想法评论加载中