Hexo添加热力图页面

Hexo添加热力图页面

参考hexo-graph,我开发的第一个npm包 | 浩瀚星河

效果展示


博客热力图
  • 点击跳转对应月份归档

月更统计
  • 点击统计点可以跳转对应月份归档

标签统计
  • 点击可以跳转对应标签

分类统计
  • 点击饼状图跳转对应分类
  • 点击下方色块可以打开和关闭该项在饼状图中的显示

安装依赖

1
2
npm i moment # 使用hexo-graph先安装相关依赖
npm i hexo-graph

修改插件内容

因为我的文章分类较少,标签较多,分类更适合饼状图,标签更适合柱状图,所以修改了插件内容,将分类和标签调换位置,并且将饼状图和柱状图调换位置,修改文件路径为node_modules/hexo-graph/lib/hexo-graph.js,可以全部替换,修改内容如下:

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
const moment = require('moment');

function generateStats(hexo) {
const posts = hexo.locals.get('posts');
const darkMode = hexo.config.hexo_graph?.theme || "light";

const monthlyCount = {};
const dailyCount = {};
const tagCount = {};
const categoryCount = {};

posts.forEach(post => {
const month = moment(post.date).format('YYYY-MM');
const day = moment(post.date).format('YYYY-MM-DD');
monthlyCount[month] = (monthlyCount[month] || 0) + 1;
dailyCount[day] = (dailyCount[day] || 0) + 1;

post.tags.data.forEach(tag => {
tagCount[tag.name] = (tagCount[tag.name] || 0) + 1;
});

post.categories.data.forEach(category => {
categoryCount[category.name] = (categoryCount[category.name] || 0) + 1;
});
});

const sortedMonthlyCount = Object.fromEntries(
Object.entries(monthlyCount)
.sort((a, b) => a[0].localeCompare(b[0]))
);

const sortedDailyCount = Object.entries(dailyCount)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([date, count]) => [date, count]);

const topTags = Object.entries(tagCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 8)
.map(([name, count]) => ({ name, count }));

const topCategories = Object.entries(categoryCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([name, count]) => ({ name, count }));

hexo.locals.set('chartData', {
monthlyCount: sortedMonthlyCount,
dailyCount: sortedDailyCount,
topTags,
topCategories
});

return `
${generateHeatmapChart(sortedDailyCount, darkMode)}
${generateMonthlyChart(sortedMonthlyCount, darkMode)}
${generateTagsChart(topTags, darkMode)}
${generateCategoriesChart(topCategories, darkMode)}
`;
}

function generateHeatmapChart(sortedDailyCount, darkMode) {
const data = JSON.stringify(sortedDailyCount);
return `
<script>
if(document.getElementById('heatmapChart')) {
const heatmapChart = echarts.init(document.getElementById('heatmapChart'), '${darkMode}');
const containerWidth = document.getElementById('heatmapChart').offsetWidth;
const cellSize = Math.max(Math.floor(containerWidth / 60));
heatmapChart.setOption({
tooltip: {
position: 'top',
formatter: params => \`\${params.value[0]}: \${params.value[1]} Articles\`
},
visualMap: {
min: 0,
max: Math.max(...${data}.map(item => item[1])),
calculable: false,
orient: 'horizontal',
right: '5%',
bottom: '5%',
inRange: { color: ['#FFEFD5', '#FFA07A', '#FF4500'] }
},
calendar: {
top: '20%',
left: 'center',
range: new Date().getFullYear(),
cellSize: cellSize,
splitLine: { lineStyle: { color: '#E0E0E0', width: 1 } },
itemStyle: { borderWidth: 1, borderColor: '#E0E0E0' },
dayLabel: { firstDay: 1, fontSize: 12, color: '#333', show: false },
monthLabel: { fontSize: 12, color: '#555' }
},
series: [{
type: 'heatmap',
coordinateSystem: 'calendar',
data: ${data}
}]
});
heatmapChart.on('click', function(params) {
if (params.componentType === 'series') {
const dateStr = params.value[0];
const dateParts = dateStr.split('-');
const year = parseInt(dateParts[0], 10);
const month = parseInt(dateParts[1], 10);
const formattedMonth = \`\${year}/\${String(month).padStart(2, '0')}\`;
window.location.href = '/archives/' + formattedMonth;
}
});
}
</script>
`;
}

function generateMonthlyChart(sortedMonthlyCount, darkMode) {
const months = JSON.stringify(Object.keys(sortedMonthlyCount));
const counts = JSON.stringify(Object.values(sortedMonthlyCount));
return `
<script>
if(document.getElementById('monthlyChart')) {
const monthlyChart = echarts.init(document.getElementById('monthlyChart'), '${darkMode}');
monthlyChart.setOption({
xAxis: {
type: 'category',
data: ${months},
axisLabel: { fontSize: 12 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { type: 'dashed', color: '#ccc' } }
},
series: [{
name: 'Articles',
type: 'line',
data: ${counts},
smooth: true,
lineStyle: { color: '#5470C6', width: 2 },
itemStyle: { color: '#5470C6' },
areaStyle: { color: 'rgba(84, 112, 198, 0.4)' },
symbolSize: 10,
animationDuration: 1000
}]
});
monthlyChart.on('click', function(params) {
if (params.componentType === 'series') {
const year = params.name.split('-')[0];
const month = params.name.split('-')[1];
window.location.href = '/archives/' + year + '/' + month;
}
});
}
</script>
`;
}

function generateCategoriesChart(topTags, darkMode) {
const tags = JSON.stringify(topTags.map(tag => ({ name: tag.name, value: tag.count })));
return `
<script>
if(document.getElementById('categoriesChart')) {
const categoriesChart = echarts.init(document.getElementById('categoriesChart'), '${darkMode}');
categoriesChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
series: [{
type: 'pie',
radius: '60%',
data: ${tags},
label: {
position: 'outside',
formatter: '{b} {c} ({d}%)',
fontSize: 12
},
color: ['#5470C6', '#91CC75', '#FAC858', '#EE6666', '#73C0DE', '#3BA272', '#FC8452', '#9A60B4'],
animationDuration: 1000
}],
legend: {
bottom: '0',
left: 'center',
data: ${tags}.map(tag => tag.name),
textStyle: { fontSize: 12 }
}
});
categoriesChart.on('click', function(params) {
if (params.componentType === 'series') {
const tag = params.name;
window.location.href = '/tags/' + tag;
}
});
}
</script>
`;
}

function generateTagsChart(topCategories, darkMode) {
const categories = JSON.stringify(topCategories.map(category => ({ name: category.name, value: category.count })));
return `
<script>
if(document.getElementById('tagsChart')) {
const tagsChart = echarts.init(document.getElementById('tagsChart'), '${darkMode}');
tagsChart.setOption({
xAxis: {
type: 'value',
splitLine: { lineStyle: { type: 'dashed', color: '#ccc' } }
},
yAxis: {
type: 'category',
data: ${categories}.map(category => category.name).reverse(),
axisLabel: { fontSize: 12 }
},
series: [{
name: 'Category Count',
type: 'bar',
data: ${categories}.map(category => category.value).reverse(),
label: {
show: true,
position: 'right',
fontSize: 12
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#91CC75' },
{ offset: 1, color: '#73C0DE' }
])
},
animationDuration: 1000
}]
});
tagsChart.on('click', function(params) {
if (params.componentType === 'series') {
const category = params.name;
window.location.href = '/categories/' + category;
}
});
}
</script>
`;
}

module.exports = { generateStats };

修改推送规则

因为博客源代码托管在github,每次更新都会重新安装插件,所以需要修改忽略规则,在.gitignore中添加以下行,以排除其他 node_modules 文件夹中的内容,但保留 hexo-graph:

1
2
node_modules/*
!node_modules/hexo-graph/

主题配置

在_config.yml中插入:

1
2
hexo_graph:
theme: "light" #light/dark 不设置或不填默认是light

使用

新建页面

运行

1
hexo new page "本站热力图"

或者在Hexo/source下新建目录statistics,目录下新建文件index.md

页面设置

编辑新建的index.md内容,其中每一条内容都可以单独插入到任意页面

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
---
title: 本站统计数据
top_img:
date:
type: statistics
updated:
comments: false
description: false
keywords: false
mathjax:
katex:
aside:
aplayer:
highlight_shrink:
top_single_background:
---

> <center><b>博客热力图</b></center>

<div
id="heatmapChart"
style="width: 100%; height: 200px; margin: 0 auto; border-radius: 5px; padding: 5px;box-shadow: 0 2px 10px #ffa500;"
></div>

> <center><b>月更统计</b></center>

<div
id="monthlyChart"
style="width: 100%; height: 350px; margin: 0 auto; border-radius: 5px; padding: 5px; box-shadow: 0 2px 10px #ffa500;"
></div>

> <center><b>标签统计</b></center>

<div
id="tagsChart"
style="width: 100%; height: 400px; margin: 0 auto; border-radius: 5px; padding: 5px; box-shadow: 0 2px 10px #ffa500;"
></div>

> <center><b>分类统计</b></center>

<div
id="categoriesChart"
style="width: 100%; height: 350px; margin: 0 auto; border-radius: 5px; padding: 5px; box-shadow: 0 2px 10px #ffa500;"
></div>

启用页面

在安知鱼主题配置_config.anzhiyu.yml中新建menu项目:

1
2
3
4
5
6
7
8
menu:
文章:
时间顺序: /archives/ || anzhiyu-icon-box-archive
分类: /categories/ || anzhiyu-icon-shapes
标签: /tags/ || anzhiyu-icon-tags
# 新建下面的内容,注意对齐
站点:
热力图: /statistics/ || anzhiyu-icon-square-poll-vertical # 图标是安知鱼的
这是我自己搭建的blog网站,用来记录和分享我自己研究的健身造型知识,类似于一个知识库。可以把这个网站分享给你身边的健身爱好者,转载或节选引,用务必注明为“李瑶的原创”,您的支持和尊重是我更新的动力!
理型健