前言

之前根据蜗牛哥的教程弄了一版使用Cal-heatmap库生成的热力图 博客更新热力图,不过这个方式需要加载四五个js文件还有一些css文件,比较拖慢整个网页的加载速度,所以蜗牛哥后来又出了一个CSS 和 JS 实现博客热力图 方案,不过当时我闲麻烦一直没跟着做。

不过我也一直在思考该怎么用纯CSS和JS更为简便的实现这个功能,今天灵光一闪,有了些思路,所以趁着这股热乎劲把这个功能实现了,撰文分享一下我的思路。

思路

我的思路其实很简单,因为博客上显示的热力图其实就是一个很多小方块组成的方阵,所以其实不用管什么年份、月份之类的,我们只需生成固定个数的小方块,再将匹配的数据填充进去不就好了吗?

我的构思如下:

  • 生成一个 7 * 9 列的格子方阵,一共是是63个
  • 每个格子要匹配往后63天中对应一天的文章
    • 如果开始当天不是星期天,则需要找到这个星期的星期天作为开始时间
  • 在生成格子的时候将文章数据附加进小格子内。
  • 完成

实现

💡
有了ChatGPT后折腾博客真的好方便呀,你只需要有一些基础编程知识和思维就能通过它构建出自己想要的代码。

不得不感慨一句:
以后的编码工作真的是看创意和思想,以及如何通过ChatGPT更快速、准确的生成自己想要的代码。

定义html结构

  • 共9列 grid-colum ,每列7个 grid-tem
  • 每个 grid-item 中有一个 item-info 用于展示信息
  • item-info 中根据需要设置选项
    • 背景颜色
    • 鼠标提示信息
    • 日期信息
    • ...
<strong>热力图</strong>
<div class="relitu-container" id="relitu-container">
  <div class="grid-column">
    <div class="grid-item">
      <div style="background-color:rgba(77, 208, 90,0.8674)" class="item-info item-tippy" data-tippy-content="共 1 篇,共 3337 字<br />- <a href='https://1900.live/i-also-run-a-half-marathon-finish-the-race/'>我也跑个半程马拉松:完赛</a></br>" data-date="2024-04-08" aria-expanded="false"></div>
    </div>
  </div>
</div>

html结构

设置CSS

这里通过 grid 布局实现了Heatmap的效果,我为了省事直接通过 after 搭配 content 在最后几个方块添加了星期标注。

.relitu-container {
    display: grid;
    grid-template-columns: repeat(9, 18px);
    grid-template-rows: repeat(7, 18px);
    gap: 5px;
    margin: 1em 0;
    position: relative;

    .grid-column {
        display: grid;
        gap: 5px;
    }

    .grid-item {
        align-items: center;
        display: flex;
        justify-content: center;
        min-height: 18px;
        position: relative;

        a{
            color:var(--hint-color-info)
        }

    }

    .item-info {
        font-size: 10px;
        height: 100%;
        width: 100%;
        background-color: var(--gray-200);
        border-radius: .25rem;

        font-size: 10px;
        height: 100%;
        width: 100%;
    }

    div:nth-child(9) > div:after{
        position: absolute;
        right: -20px;
        color: var(--gray-500);
    }

    div:nth-child(9) > div:nth-child(1):after{
        content: "一";

    }
    
    div:nth-child(9) > div:nth-child(3):after{
        content: "三";
    }
    
    div:nth-child(9) > div:nth-child(5):after{
        content: "五";
    }
    
    
    div:nth-child(9) > div:nth-child(7):after{
        content: "日";
    }

}

scss样式

JSON数据部分

这边可以根据你的需求生成这部分数据

[
  {
    "id": 1,
    "href": "https://1900.live/pcshi-yong-de-appxin-xi-tong-bu-geng-xin-dao-bo-ke-shang/",
    "date": "2024-05-13T23:00:16.000+08:00",
    "title": "将博主PC上使用的应用信息实时显示到博客",
    "section": "编码,工具箱,分享",
    "published": "2024-05-13T23:00:16.000+08:00",
    "word_count": 13037
  }
  //...Items
]

json数据格式

JS部分

几乎所有代码都是通过ChatGPT生成,相关说明我直接写在代码中。

// 字符串转换为时间格式
function parseDate(str) {
    return new Date(str);
}

// 获取本周的星期天作为开始时间
function getThisSunday(date) {
    const dayOfWeek = date.getDay();
    const daysToSunday = dayOfWeek === 0 ? 0 : 7 - dayOfWeek;
    const thisSunday = new Date(date);
    thisSunday.setDate(thisSunday.getDate() + daysToSunday);
    return thisSunday;
}

const today = new Date();
const sunday = getThisSunday(today);
let startDate = new Date(sunday);
// 往后推63天
startDate.setDate(sunday.getDate() - 62);

// 构建基础数据
function dateBuild(data) {
    const dateCounts = {};
    // 标准化json中的时间格式
    data.forEach((item) => {
        const dateStr = parseDate(item.date).toISOString().split("T")[0];
        dateCounts[dateStr] = (dateCounts[dateStr] || 0) + 1;
    });

    const result = [];
    // 生成63天的数据数组
    for (
        let currentDate = sunday;
        currentDate >= startDate;
        currentDate.setDate(currentDate.getDate() - 1)
    ) {
        const dateStr = currentDate.toISOString().split("T")[0];
        const count = dateCounts[dateStr] || 0;
        // 通过时间去json数据获取当天的文章
        const dataContent = data.filter(
            (item) =>
                parseDate(item.date).toISOString().split("T")[0] === dateStr
        );

        // 放进数组中
        result.push({
            date: dateStr,
            count: count,
            data: dataContent,
        });
    }

    // 统计文章字符总数(好像可以整合进上面的循环中,下次再优化把。
    result.forEach((item) => {
        var sumOfWordcounts = item.data.reduce((accumulator, currentItem) => {
            return (accumulator = currentItem.word_count || 0);
        }, 0);
        item.wordcount = sumOfWordcounts;
    });

    return result;
}

// 填充数据
export default function fillGrid(data) {
    // 先将构建用于渲染的数据
    let articles = dateBuild(data);
    // 获取热力图元素
    const gridContainer = document.getElementById("relitu-container");
    // 构建grid-item元素
    const gridItemTemplate = document.createElement("div");
    gridItemTemplate.className = "grid-item";

    // 倒序遍历文章数据
    articles
        .slice()
        .reverse()
        .forEach((article, index) => {
            const gridItem = gridItemTemplate.cloneNode(false);
            // 构建提示字符串
            const tooltipStr = article.data
                .map(
                    (item, i) =>
                        `- <a href='${item.href}'>${item.title}</a></br>`
                )
                .join(" ");
            // 构建grid-info中的信息
            // 关于小方框颜色部分,我这里直接用的百分比透明度,起步0.2,5000字100%
            gridItem.innerHTML = `<div style="${ article.wordcount != 0 ? `background-color:rgba(77, 208, 90,${article.wordcount / 5000 + 0.2})`:""}" 
            class="item-info ${ article.count != 0 ? `item-tippy" data-tippy-content="共 ${article.count} 篇,共 ${article.wordcount} 字<br />${tooltipStr}"`:'"' } data-date="${article.date}"></div>`;

            // 计算排列顺序
            const colIndex = Math.floor(index / 7);
            const rowIndex = index % 7;

            if (rowIndex === 0) {
                const gridColumn = document.createElement("div");
                gridColumn.className = "grid-column";
                gridContainer.appendChild(gridColumn);
            }
            // 根据顺序构建
            const gridColumns = document.getElementsByClassName("grid-column");
            if (gridColumns[colIndex]) {
                gridColumns[colIndex].append(gridItem);
            } else {
                // 如果列索引超出了当前已有的列,需要创建新的列
                const newColumn = document.createElement("div");
                newColumn.className = "grid-column";
                gridContainer.appendChild(newColumn);
                newColumn.append(gridItem);
            }
        });
}

我这里用的fetch加载的json文件,刚刚测试了一下速度还是没有直接将json生成在html中速度快,下一版做一下改动。

document.addEventListener("DOMContentLoaded", function () {
    fetch("/assets/relitu-data.json")
        .then((respone) => respone.json())
        .then((posts) => {
            // 现在使用dateBuild函数处理数据,并将结果传递给fillGrid函数
            fillGrid(posts);
            tippy(".item-tippy", { allowHTML: true, interactive: true ,maxWidth: 'none',appendTo: () => document.body,});
        });
});

入口函数调用

Enjoy~