惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Blog — PlanetScale
Blog — PlanetScale
爱范儿
爱范儿
MongoDB | Blog
MongoDB | Blog
腾讯CDC
aimingoo的专栏
aimingoo的专栏
月光博客
月光博客
Engineering at Meta
Engineering at Meta
C
Check Point Blog
N
Netflix TechBlog - Medium
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LangChain Blog
大猫的无限游戏
大猫的无限游戏
IT之家
IT之家
Microsoft Security Blog
Microsoft Security Blog
GbyAI
GbyAI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
The Blog of Author Tim Ferriss
Last Week in AI
Last Week in AI
B
Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
博客园 - 叶小钗
WordPress大学
WordPress大学
博客园 - 司徒正美

素生

2026 阅读 记时过夏末2026巡演成都站 观后:《忘梦洞》 最近惦念 20260322 摘:《How to Do Nothing》 觉得会离不开的供应商盘点(2026年3月) 2026 纸书单 OpenClaw 配置 最近惦念 20260213 最近惦念 20260128 最近惦念 20260110 摘:《永恒的时光之旅》 摘:《移民》 摘:《未弃之物》 摘:《建筑养成记》 最近惦念 20251230 佳 2025 2025 阅读 2025 观影 最近惦念 20251211 最近惦念 20251125 普鲁斯特问卷 2025 最近惦念 20251110 摘:《三诗人书简》 摘:《彼岸》 摘:《熊猫的拇指》 最近惦念 20251025 最近惦念 20251013 入蜀记 day145 有依据的复盘 入蜀记 day144 打球被咬
Hexo 增加热力图
素生 · 2026-03-19 · via 素生
1
2
3
4
5
6
7
8
9
10
block content
.post
h1.post-title(onclick="copyUrlSegment()")= page.title

//- 放在正文内容之前(推荐,一打开页面就能看到热力图)
if page.heatmap
include _partial/heatmap.pug

.post-content
!= page.content
1
2
3
4
5
6
7
8
9
10
11
12
block content
.post
h1.post-title(onclick="copyUrlSegment()")= page.title
.post-meta= page.date.format(config.date_format)

//- 放在.post-meta之后.post-content之前,与它们平级
if page.heatmap
div(style="margin: 2rem 0;")
include _partial/heatmap.pug

.post-content
!= page.content
1
2
3
4
5
---
title: INDEX
date: 2025-09-30 09:08:27
heatmap: true
---
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
//- 1. 数据预处理
-
var yearWeekData = {};
var yearsSet = new Set();

site.posts.forEach(function(post) {
var y = post.date.isoWeekYear();
var w = post.date.isoWeek();
yearsSet.add(y);

var key = y + '-' + w;
if (!yearWeekData[key]) {
yearWeekData[key] = { count: 0, posts: [] };
}
yearWeekData[key].count += 1;
yearWeekData[key].posts.push({
title: post.title,
link: url_for(post.path)
});
});

var allYears = Array.from(yearsSet).sort(function(a, b){ return b - a });
if(allYears.length === 0) allYears = [new Date().getFullYear()];
var maxYear = allYears[0];
var minYear = allYears[allYears.length - 1];

var displayYears = [];
for(var i = maxYear; i >= minYear; i--) {
displayYears.push(i);
}

//- 2. DOM 结构
#heatmap-container
#tooltip

//- 3. 样式表
style.
#heatmap-container {
width: 100%;
overflow: visible;
margin-top: 40px;
margin-bottom: 30px;
}

.heatmap-cell {
rx: 1.5px;
ry: 1.5px;
transition: opacity 0.2s ease;
stroke: none !important;
}

.heatmap-cell.has-post {
cursor: default; /* 改为默认箭头,因为现在不直接点击色块了 */
}

.heatmap-cell:hover {
opacity: 0.5;
}

.year-text, .legend-text, .legend-number, #tooltip {
font-family: 'Source Han Serif SC', 'Source Han Serif', 'Noto Serif CJK SC', serif;
}

.year-text {
font-size: 14px;
font-weight: bold;
fill: #888888;
}

.legend-text {
font-size: 14px;
fill: #333333;
font-weight: bold;
}

.legend-number {
font-size: 13px;
fill: #888888;
}

html[data-dark="true"] .legend-text { fill: #dddddd; }
html[data-dark="true"] .year-text,
html[data-dark="true"] .legend-number { fill: #777777; }

/* Tooltip 样式:删除了 pointer-events: none,加入隐藏机制 */
#tooltip {
position: absolute;
background-color: rgba(255, 255, 255, 0.98);
color: #222222;
border: 1px solid #e0e0e0;
border-radius: 2px;
padding: 10px 14px;
opacity: 0;
visibility: hidden; /* 默认隐藏,防止阻挡正常页面点击 */
font-size: 13px;
transform: translate(-50%, -100%);
line-height: 1.6;
z-index: 1000;
white-space: nowrap;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}

/* 优雅的链接样式 */
#tooltip a {
color: inherit;
text-decoration: none;
display: block;
border-bottom: 1px dashed rgba(136, 136, 136, 0.3);
padding: 2px 0;
transition: all 0.2s ease;
}
#tooltip a:last-child {
border-bottom: none;
}
#tooltip a:hover {
color: #000;
border-bottom: 1px solid #000;
}

html[data-dark="true"] #tooltip {
background-color: rgba(30, 30, 30, 0.98);
color: #dddddd;
border: 1px solid #444444;
box-shadow: 0 4px 12px rgba(0,0,0,0.6);
}

html[data-dark="true"] #tooltip a:hover {
color: #fff;
border-bottom: 1px solid #fff;
}

//- 4. 脚本逻辑
script(src="https://d3js.org/d3.v5.min.js")
script.
document.addEventListener("DOMContentLoaded", function () {
var data = !{JSON.stringify(yearWeekData)};
var displayYears = !{JSON.stringify(displayYears)};

var fullGrid = [];
displayYears.forEach(function(y, rowIdx) {
for(var w = 1; w <= 53; w++) {
var key = y + '-' + w;
var cellData = data[key] || { count: 0, posts: [] };
fullGrid.push({
year: y,
week: w,
row: rowIdx,
col: w - 1,
count: cellData.count,
posts: cellData.posts
});
}
});

var cellWidth = 10;
var cellHeight = 16.18;
var cellPaddingX = 3;
var cellPaddingY = 8;
var labelWidth = 50;
var topMargin = 45;

var width = labelWidth + (53 * (cellWidth + cellPaddingX));
var height = topMargin + (displayYears.length * (cellHeight + cellPaddingY));

var svg = d3.select("#heatmap-container").append("svg")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMidYMid meet");

function getFillColor(count, isDark) {
if (isDark) {
if (count === 0) return "#1e1e1e";
if (count === 1) return "#3a3a3a";
if (count === 2) return "#5c5c5c";
if (count === 3) return "#858585";
return "#b3b3b3";
} else {
if (count === 0) return "#ebebeb";
if (count === 1) return "#c2c2c2";
if (count === 2) return "#949494";
if (count === 3) return "#666666";
return "#333333";
}
}

svg.append("text")
.attr("class", "legend-text")
.attr("x", 0)
.attr("y", 16)
.text("Posts by week");

var legendCounts = [0, 1, 2, 3, 4];
var legendGroup = svg.append("g").attr("transform", "translate(125, 4)");
var legendSpacing = 42;

legendGroup.selectAll("rect")
.data(legendCounts)
.enter().append("rect")
.attr("class", "heatmap-cell")
.attr("width", cellWidth)
.attr("height", cellHeight)
.attr("x", function(d, i) { return i * legendSpacing; })
.attr("y", 0);

legendGroup.selectAll("text")
.data(["0", "1", "2", "3", "4+"])
.enter().append("text")
.attr("class", "legend-number")
.attr("x", function(d, i) { return i * legendSpacing + cellWidth + 6; })
.attr("y", 13)
.text(function(d) { return d; });

svg.selectAll(".year-text.axis")
.data(displayYears)
.enter().append("text")
.attr("class", "year-text axis")
.attr("x", 0)
.attr("y", function(d, i) {
return topMargin + i * (cellHeight + cellPaddingY) + cellHeight - 3;
})
.text(function(d) { return d; });

var gridGroup = svg.append("g")
.attr("transform", "translate(" + labelWidth + "," + topMargin + ")");

var cells = gridGroup.selectAll(".heatmap-cell.grid-cell")
.data(fullGrid)
.enter().append("rect")
.attr("class", function(d) { return "heatmap-cell grid-cell" + (d.count > 0 ? " has-post" : ""); })
.attr("width", cellWidth)
.attr("height", cellHeight)
.attr("x", function(d) { return d.col * (cellWidth + cellPaddingX); })
.attr("y", function(d) { return d.row * (cellHeight + cellPaddingY); });

// 【核心改进】:增加缓冲计时器,解决鼠标移动时的断层消失问题
var hideTimeout;
var tooltip = d3.select("#tooltip");

cells.on("mouseover", function(d) {
if (d.count > 0) {
clearTimeout(hideTimeout);
tooltip.style("visibility", "visible").transition().duration(200).style("opacity", 1);

// 渲染顶部周次说明,以及下方的文章可点击链接
var text = "<div style='margin-bottom: 8px; font-weight: bold; color: #888; font-size: 11px; border-bottom: 1px solid #eee; padding-bottom: 4px;'>" + d.year + "年 第" + d.week + "周 (" + d.count + "篇)</div>";
d.posts.forEach(function(p) {
text += "<a href='" + p.link + "'>" + p.title + "</a>";
});
tooltip.html(text);

var cellBox = this.getBoundingClientRect();
tooltip.style("left", (cellBox.left + cellBox.width / 2) + "px")
.style("top", (cellBox.top - 8 + window.scrollY) + "px");
}
})
.on("mouseout", function() {
// 离开色块时,给用户留出 250 毫秒的移动缓冲时间
hideTimeout = setTimeout(function() {
tooltip.transition().duration(200).style("opacity", 0).on("end", function() {
d3.select(this).style("visibility", "hidden");
});
}, 250);
});

// 为 Tooltip 本身增加事件,确保鼠标悬停在框内时不消失
tooltip.on("mouseover", function() {
clearTimeout(hideTimeout);
d3.select(this).style("opacity", 1).style("visibility", "visible");
})
.on("mouseout", function() {
// 鼠标离开悬浮框,彻底隐藏
hideTimeout = setTimeout(function() {
tooltip.transition().duration(200).style("opacity", 0).on("end", function() {
d3.select(this).style("visibility", "hidden");
});
}, 250);
});

function updateCellStyles() {
var isDarkTheme = document.querySelector('html').dataset.dark === 'true';
legendGroup.selectAll("rect").style("fill", function(d, i) {
return getFillColor(i, isDarkTheme);
});
cells.style("fill", function(d) {
return getFillColor(d.count, isDarkTheme);
});

// 暗黑模式下,Tooltip 内的分割线也要适应变暗
if(isDarkTheme) {
document.querySelectorAll('#tooltip div').forEach(el => el.style.borderBottom = '1px solid #444');
} else {
document.querySelectorAll('#tooltip div').forEach(el => el.style.borderBottom = '1px solid #eee');
}
}

updateCellStyles();

document.body.addEventListener("themechange", function () {
updateCellStyles();
});
});