1 目标效果
举个例子,Github 中的热力图大家最熟悉了。我们根据热力图中不同方块颜色,可以了解作者在社区的活跃程度,是一种很好的可视化工具。
在Hugo中,如果能可视化文章的更新活动,可以让读者对网站的更新动态有更具体的了解,因此我们看到热力图是比较有实用价值的,下面我们来看看我是如何实现该模块的。
2 获取总天数
想要绘制的方格子,首先我们计算需要多少个数据,写出 DaysMount_XMonthesBefore 方法如下,用以计算要显示的天数。
{{ define "partials/DaysMount_XMonthesBefore.html" }}
{{ $x_mon_bef := int .x_mon_bef }}
{{ $DaysMount := time.Now.Sub (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ return (div ($DaysMount).Hours 24) }}
{{ end }}
1.1 封装函数
使用 Hugo 提供的 partials 封装函数,该函数的作用是获取要显示的天数。
1.2 传入参数
$x_mon_bef := int .x_mon_bef 语句用于接收输入的参数,含义为“从前X个月的第1天开始计算到今天的天数”,如X为1,假设今天是5月2日,则获取的是4月1日到5月2日的总天数。
1.3 计算时间差
time.Now.AddDate 函数表示当前时间加上给定的量,Hugo文档对该方法的描述如下:
{{ $d := "2022-01-01" | time.AsTime }}
{{ $d.AddDate 0 0 1 | time.Format "2006-01-02" }} → 2022-01-02
{{ $d.AddDate 0 1 1 | time.Format "2006-01-02" }} → 2022-02-02
{{ $d.AddDate 1 1 1 | time.Format "2006-01-02" }} → 2023-02-02
{{ $d.AddDate -1 -1 -1 | time.Format "2006-01-02" }} → 2020-11-30
time.Now.Sub 函数计算当前时间和给定X个月之前的第一天之间的时间差,返回类型见如下:
{{ $t1 := time.AsTime "2023-01-27T23:44:58-08:00" }}
{{ $t2 := time.AsTime "2023-01-26T22:34:38-08:00" }}
{{ $t1.Sub $t2 }} → 25h10m20s
1.4 返回日期差
对于 25h10m20s 这种形式的返回,使用 div ($DaysMount).Hours 24 函数计算天数并返回变量。
2 绘制方格子
现在我们获得了总共有多少个方格子,下面我们依次把它们画出来。
{{ $x_mon_bef := -3 }}
{{ range seq 0 (partial "DaysMount_XMonthesBefore.html" (dict "x_mon_bef" $x_mon_bef)) }}
{{ end }}
{{ define "partials/DaysMount_XMonthesBefore.html" }}
{{ $x_mon_bef := int .x_mon_bef }}
{{ $DaysMount := time.Now.Sub (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ return (div ($DaysMount).Hours 24) }}
{{ end }}
2.1 先画出来所有格子再说
想到我们需要表格形式绘制方格,如果使用 table 会有大量的 tr 和 td ,且我们是遍历的日期,日期之间的层级关系很难组织(画完这个 tr 中的 td 又要转到下一个 tr 中的 td ,之后再回到这个 tr 中的 td ,这是一件很麻烦的事),因此这里使用 grid 组织格子。
我在样式层使用了 TailwindCSS,这是一个很方便的库,把 CSS 样式组织到了类中,如果你要读懂我下面的代码,应该对 TailwindCSS 有些了解。
{{/* .../input.css # this is input file of tailwindcss */}}
.card{
@apply bg-white border shadow-md rounded-lg
}
{{/* .../partials/calendar.html */}}
<div class="card w-[400px] h-80">
<div class="square-month grid grid-rows-7 gap-1 p-3">
{{ $x_mon_bef := -1 }}
{{ range seq 0 (partial "DaysMount_XMonthesBefore.html" (dict "x_mon_bef" $x_mon_bef)) }}
{{ end }}
</div>
</div>
{{ define "partials/DaysMount_XMonthesBefore.html" }}
{{ $x_mon_bef := int .x_mon_bef }}
{{ $DaysMount := time.Now.Sub (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ return (div ($DaysMount).Hours 24) }}
{{ end }}
下面我们把方格子的渲染写入循环,每个循环渲染一个小方格子。
{{/* .../input.css # this is input file of tailwindcss */}}
.card{
@apply bg-white border shadow-md rounded-lg
}
.square{
@apply w-[16px] h-[16px] overflow-visible rounded-sm shadow-inner;
}
.square-level0{
@apply bg-gray-200 shadow-gray-400/50 hover:shadow-gray-400/50
}
{{/* .../partials/calendar.html */}}
<div class="card w-[400px] h-80">
<div class="square-month grid grid-rows-7 gap-1 p-3">
{{ $x_mon_bef := -1 }}
{{ range seq 0 (partial "DaysMount_XMonthesBefore.html" (dict "x_mon_bef" $x_mon_bef)) }}
<div class="square square-level0 overflow-hidden">
{{ . }}
</div>
{{ end }}
</div>
</div>
{{ define "partials/DaysMount_XMonthesBefore.html" }}
{{ $x_mon_bef := int .x_mon_bef }}
{{ $DaysMount := time.Now.Sub (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ return (div ($DaysMount).Hours 24) }}
{{ end }}
我们可以看到代码运行的效果如下:
2.2 设置格子的行和列
所有的格子都从第一行直接排到了最后一行,我们想把格子按照 grid 排列,即给定其 grid-rows-start 和 grid-column-start 值。
观察我们的目标效果图可以知道,我们需要 7 行,每一行都是相同的星期几,比如第一行都是星期日,第二行都是星期六;而每一列就是一周,从星期日、星期六到星期一,假设第一列是该月第一周,第二列是该月第二周。
我们假设每个月一号都是从星期日开始的,这样我们可以用得到的数字除以 7,余数相同的表示其在同一周,放在同一列,即用第几周表示 grid-column-start。
同理,我们可以用星期几表示 grid-rows-start ,我使用了星期值(Weekday 转为 int 类型,见文档)表示 grid-rows-start ,下面我们开始行动。
我们首先获得格子对应的日期,我们只要获得最早的那个日期,之后的日期就在上面加格子代表的整数值即可。
<div class="card w-[400px] h-80">
<div class="square-month grid grid-rows-7 gap-1 p-3">
{{ $x_mon_bef := -1 }}
{{ range seq 0 (partial "DaysMount_XMonthesBefore.html" (dict "x_mon_bef" $x_mon_bef)) }}
{{ $first_day := (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ $row_start := (($first_day.AddDate 0 0 .).Weekday | int) }}
{{ $col_start := div . 7 }}
<div class="square square-level0 overflow-hidden"
style="grid-row-start: {{ add 1 $row_start }}; grid-column-start: {{ add 1 $col_start }}">
{{ . }}
</div>
{{ end }}
</div>
</div>
{{ define "partials/DaysMount_XMonthesBefore.html" }}
{{ $x_mon_bef := int .x_mon_bef }}
{{ $DaysMount := time.Now.Sub (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ return (div ($DaysMount).Hours 24) }}
{{ end }}
现在我们得到了如图所示的效果:
2.3 修正错误
我们发现 0 - 5 显示都是正常的,这六个点是根据星期几算出来的行坐标,但 6 的列坐标提前了一个列,显示在了第一个列上,这是因为我们假设每个月一号都是从星期日开始的,用 div . 7 计算列必然会导致这一粗糙的结果,现在我们来修正这一点。
我们将 0 之前的格子用上一个月份的剩余几天填充,就像看日历一样,这个月的日历总会显示上个月的最后几天。
<div class="card w-[400px] h-80">
<div class="square-month grid grid-rows-7 gap-1 p-3">
{{ $x_mon_bef := -1 }}
{{ $flag := 1 }}
{{ range seq 0 (partial "DaysMount_XMonthesBefore.html" (dict "x_mon_bef" $x_mon_bef)) }}
{{ $first_day := (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ $row_start := (($first_day.AddDate 0 0 .).Weekday | int) }}
{{ $col_start := div . 7 }}
{{ $date_before := $row_start }}
{{ if (and (gt $date_before 0) $flag) }}
{{ range seq 0 $date_before }}
<div class="square overflow-hidden"
style="grid-row-start: {{ add 1 . }}; grid-column-start: 1">
{{ . }}
</div>
{{ end }}
{{ else }}
<div class="square square-level0 overflow-hidden"
style="grid-row-start: {{ add 1 $row_start }}">
{{ . }}
</div>
{{ end }}
{{ $flag = 0 }}
{{ end }}
</div>
</div>
{{ define "partials/DaysMount_XMonthesBefore.html" }}
{{ $x_mon_bef := int .x_mon_bef }}
{{ $DaysMount := time.Now.Sub (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ return (div ($DaysMount).Hours 24) }}
{{ end }}
我们首先要把19行的 grid-column-start 去掉,因为我们要用填充的格子占位置,剩下的格子从上往下,从左往右依次占位置即可。如果不去掉就会在这个位置产生格子冲突。
我们用 $flag 变量记录是否补过格子,如果补过了格子,那么 $flag 就置为 0,否则其为默认值 1.
我们设置 $date_before 变量,如果 $row_start 从一开始就大于0,说明其前几行一定需要用上一个月的数据补全, $data_before 为几就代表需要补全几个位置。
但是,这里 $row_start 值为 1,因此遍历 0 和 1 两个值,我们明明需要补一个位置,这里为什么遍历两次呢?原来这里要注意一下,我们借助第一次遍历补充格子,那么第一次遍历是不会输出原来的格子的,所以我们输出的灰色格子是少一个的,因此我们在补充格子的时候要多补充一个!
通过下方输出的效果图我们可以看到,白色小方格就是我们补充的格子,到现在为止,我们已经正确地输出了格子
3 连接方格子和日期
现在我们输出 {{ . }} ,只能看到数字,下面我们要把数字和日期对应起来。我们将对应的值和 $firstday 相加就可以获得日期,现在我们把小格子的长度拉长一些,以方便看到日期的显示。
<div class="card w-[400px] h-80">
<div class="square-month grid grid-rows-7 gap-1 p-3">
{{ $x_mon_bef := -1 }}
{{ $first_day := (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ $flag := 1 }}
{{ range seq 0 (partial "DaysMount_XMonthesBefore.html" (dict "x_mon_bef" $x_mon_bef)) }}
{{ $theDate := (($first_day.AddDate 0 0 .) | time.Format "2006-01-02") }}
{{ $row_start := (($first_day.AddDate 0 0 .).Weekday | int) }}
{{ $col_start := div . 7 }}
{{ $date_before := $row_start }}
{{ if (and (gt $date_before 0) $flag) }}
{{ range seq 0 $date_before }}
{{ $theDate = ($first_day.AddDate 0 0 (int (sub . $date_before))) | time.Format "2006-01-02" }}
<div class="w-20 square-level0"
style="grid-row-start: {{ add 1 . }}; grid-column-start: 1">
{{ $theDate }}
</div>
{{ end }}
{{ else }}
<div class="w-20 square-level0"
style="grid-row-start: {{ add 1 $row_start }}">
{{ $theDate }}
</div>
{{ end }}
{{ $flag = 0 }}
{{ end }}
</div>
</div>
{{ define "partials/DaysMount_XMonthesBefore.html" }}
{{ $x_mon_bef := int .x_mon_bef }}
{{ $DaysMount := time.Now.Sub (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ return (div ($DaysMount).Hours 24) }}
{{ end }}
显示日期的效果图如下,我们看到日期和数字已经完美得对应起来了。
4 调整美化
现在我们把日期弄到悬浮窗上,当鼠标悬浮在小方格上的时候,悬浮窗出现并显示日期,鼠标移走时悬浮窗消失,这样会很美观。
同时,我们把日期增多一些,从 1 个月增加到 3 个月,这样显示的小方块更多一些,同时调整一下边框的大小,让它们看起来更顺眼。
{{/* .../input.css # this is input file of tailwindcss */}}
.square-tip{
@apply w-32 h-fit p-2 -translate-x-16 -translate-y-11 bg-white shadow-lg relative top-2 hidden text-xs text-center rounded-lg
}
.square:hover .square-tip{
@apply block
}
{{/* .../partials/calendar.html */}}
<div class="card w-[300px] h-fit">
<div class="square-month grid grid-rows-7 gap-1 p-3">
{{ $x_mon_bef := -3 }}
{{ $first_day := (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ $flag := 1 }}
{{ range seq 0 (partial "DaysMount_XMonthesBefore.html" (dict "x_mon_bef" $x_mon_bef)) }}
{{ $theDate := (($first_day.AddDate 0 0 .) | time.Format "2006-01-02") }}
{{ $row_start := (($first_day.AddDate 0 0 .).Weekday | int) }}
{{ $col_start := div . 7 }}
{{ $date_before := $row_start }}
{{ if (and (gt $date_before 0) $flag) }}
{{ range seq 0 $date_before }}
{{ $theDate = ($first_day.AddDate 0 0 (int (sub . $date_before))) | time.Format "2006-01-02" }}
<div class="square overflow-hidden square-level0"
style="grid-row-start: {{ add 1 . }}; grid-column-start: 1">
<div class="square-tip">
{{ $theDate }}
</div>
</div>
{{ end }}
{{ else }}
<div class="square overflow-hidden square-level0"
style="grid-row-start: {{ add 1 $row_start }}">
<div class="square-tip">
{{ $theDate }}
</div>
</div>
{{ end }}
{{ $flag = 0 }}
{{ end }}
</div>
</div>
{{ define "partials/DaysMount_XMonthesBefore.html" }}
{{ $x_mon_bef := int .x_mon_bef }}
{{ $DaysMount := time.Now.Sub (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ return (div ($DaysMount).Hours 24) }}
{{ end }}
可以看到效果如下,我们已经做出了很好看的小方块日历。下面需要做的事情就是为它们加上颜色(热度),这样热度图就完成了。
5 获取每天的文章数量并用颜色渲染
我们设置 $count 变量用于统计次数,对于每个日期都遍历一次 .Site.RegularPages ,以获得该日期的 $count 值。注意由于 $theDay 变量不一样,我们需要在补全方格的时候重新计算一下要补全的日期的 $count。
{{ $site := .Site.RegularPages }}
<div class="card w-[300px] h-fit">
<div class="square-month grid grid-rows-7 gap-1 p-3">
{{ $x_mon_bef := -3 }}
{{ $first_day := (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ $flag := 1 }}
{{ range seq 0 (partial "DaysMount_XMonthesBefore.html" (dict "x_mon_bef" $x_mon_bef)) }}
{{ $theDate := (($first_day.AddDate 0 0 .) | time.Format "2006-01-02") }}
{{ $row_start := (($first_day.AddDate 0 0 .).Weekday | int) }}
{{ $col_start := div . 7 }}
{{ $date_before := $row_start }}
{{ $count := 0 }}
{{ range $site }}
{{ $ArticleDate := .Date | time.Format "2006-01-02" }}
{{ if (eq $ArticleDate $theDate) }}
{{ $count = add $count 1 }}
{{ if gt $count 7 }}
{{ $count = 7 }}
{{ end }}
{{ end }}
{{ end }}
{{ if (and (gt $date_before 0) $flag) }}
{{ range seq 0 $date_before }}
{{ $theDate = ($first_day.AddDate 0 0 (int (sub . $date_before))) | time.Format "2006-01-02" }}
{{ $count := 0 }}
{{ range $site }}
{{ $ArticleDate := .Date | time.Format "2006-01-02" }}
{{ if (eq $ArticleDate $theDate) }}
{{ $count = add $count 1 }}
{{ if gt $count 7 }}
{{ $count = 7 }}
{{ end }}
{{ end }}
{{ end }}
<div class="square overflow-hidden square-level{{ $count }}"
style="grid-row-start: {{ add 1 . }}; grid-column-start: 1">
<div class="square-tip">
{{ $count }}, {{ $theDate }}
</div>
</div>
{{ end }}
{{ else }}
<div class="square overflow-hidden square-level{{ $count }}"
style="grid-row-start: {{ add 1 $row_start }}">
<div class="square-tip">
{{ $count }}, {{ $theDate }}
</div>
</div>
{{ end }}
{{ $flag = 0 }}
{{ end }}
</div>
</div>
{{ define "partials/DaysMount_XMonthesBefore.html" }}
{{ $x_mon_bef := int .x_mon_bef }}
{{ $DaysMount := time.Now.Sub (time.Now.AddDate 0 $x_mon_bef 0)}}
{{ return (div ($DaysMount).Hours 24) }}
{{ end }}
我们增加 css 样式,对于 level0 - level7 每个等级的方格颜色由浅入深颜色不同。
.square-level1{
@apply bg-green-300 shadow-green-500/50 hover:shadow-green-900/50
}
.square-level2{
@apply bg-green-400 shadow-green-600/50 hover:shadow-green-900/50
}
.square-level3{
@apply bg-green-500 shadow-green-700/50 hover:shadow-green-900/50
}
.square-level4{
@apply bg-green-600 shadow-green-800/50 hover:shadow-green-900/50
}
.square-level5{
@apply bg-orange-600 shadow-orange-800/50 hover:shadow-orange-900/50
}
.square-level6{
@apply bg-red-500 shadow-red-700/50 hover:shadow-red-900/50
}
.square-level7{
@apply bg-red-600 shadow-black/50 hover:shadow-red-900/50
}
.square-level0{
@apply bg-gray-200 shadow-gray-400/50 hover:shadow-gray-400/50
}
.square{
@apply w-[16px] h-[16px] overflow-visible rounded-sm shadow-inner;
}
.square-tip{
@apply w-32 h-fit p-2 -translate-x-16 -translate-y-11 bg-white shadow-lg relative top-2 hidden text-xs text-center rounded-lg
}
.square:hover .square-tip{
@apply block
}
6 留以思考
如果你已经读完了这篇文章,你可以试试解决下面两个问题:当 $count 大于 7 的时候我们应该怎么处理?$count 的计算是否可以封装成函数,如何封装?