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

推荐订阅源

P
Privacy International News Feed
WordPress大学
WordPress大学
Security Latest
Security Latest
Cyberwarzone
Cyberwarzone
K
Kaspersky official blog
Cisco Talos Blog
Cisco Talos Blog
Microsoft Security Blog
Microsoft Security Blog
G
GRAHAM CLULEY
N
News | PayPal Newsroom
Apple Machine Learning Research
Apple Machine Learning Research
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Visual Studio Blog
美团技术团队
J
Java Code Geeks
I
Intezer
The Cloudflare Blog
SecWiki News
SecWiki News
S
Secure Thoughts
Microsoft Azure Blog
Microsoft Azure Blog
V2EX - 技术
V2EX - 技术
C
Cyber Attacks, Cyber Crime and Cyber Security
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Spread Privacy
Spread Privacy
D
DataBreaches.Net
S
Security Affairs
Help Net Security
Help Net Security
S
Securelist
F
Full Disclosure
C
Check Point Blog
F
Fortinet All Blogs
Know Your Adversary
Know Your Adversary
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Y
Y Combinator Blog
云风的 BLOG
云风的 BLOG
阮一峰的网络日志
阮一峰的网络日志
The Register - Security
The Register - Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
人人都是产品经理
人人都是产品经理
博客园_首页
G
Google Developers Blog
Google Online Security Blog
Google Online Security Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
H
Help Net Security
酷 壳 – CoolShell
酷 壳 – CoolShell
I
InfoQ
Application and Cybersecurity Blog
Application and Cybersecurity Blog
H
Hacker News: Front Page
L
LINUX DO - 热门话题
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LangChain Blog

eallion's Blog

春假清明自驾游 Ubuntu 25.10 安装和配置 秋假 彩礼 2025 博客变化 预制菜 联邦礼仪之一 重拾写博客的乐趣 少儿 TED - 时间管理大师 n8n 之同步博客到 Mastodon n8n 之备份 Mastodon 嘟文 如何备份 Mastodon Docker 部署 Mastodon NAS 折腾记 Windows 11 安装软件 博客排版 - 挤压中文标点符号 Hugo 博客集成 Mastodon 独立博客自省问卷 15 题 Chrome 插件更新:网址净化器 在 Hugo 中使用 Shiki 炒菜万能公式 uBlacklist 订阅合集 读《中文互联网正在加速崩塌》 受灾小记 那,他吃什么?! Mastodon 同步到 Memos Hugo 外部链接跳转提示页面 联邦宇宙及 Mastodon 简介 2024 博客变化 部署动态生成 OG Image 的 API 再看《星际穿越》 实感 自部署 GitHub 风格的 Reactions 点赞功能 图床 CDN CNAME 接入 Cloudflare SaaS 实现分流 利用 GitHub Actions 同步对象存储 留给孩子一个完整的母亲 博客 AI 摘要及优化 豆瓣同步到 Notion 和 Neodb NeoDB API 创建观影页面 Artalk 无评论随机显示诗词 孙燕姿关于AI孙燕姿的回复 Windows 安装 Rime 小狼毫五笔拼音输入法 Umami Docker 部署及优化 去有风的地方 非 24 小时睡眠觉醒障碍 Memos API 获取总条数 Memos API 公告样式滚动效果 Memos API 调用渲染页面 Memos 手动导入数据 Memos 简介 凉城利川·避暑旅居胜地(附 CCTV 报道) 珊瑚鱼 读《中文大约的确已经死了》 再说评论 且试天下 记一次博客被攻击 Hugo .GitInfo 的替代方案 Gitea 安装备忘 Twikoo 集成 Slimbox2 灯箱插件 童心皆可爱 减肥小结 白粽肉粽及端午快乐安康 劳动合同解除 (终止) 及赔偿一览表 启用 Waline 静态博客评论系统的选择 好好说话 KMS Windows 激活服务器 Ubuntu 20.10 优化 关于 Ubuntu Ubuntu ZFS 原生全盘加密 Ubuntu ZFS 加密 Home 目录 爱丽丝梦游仙境症 偶发 月半 Ubuntu 20.04.1 配置 LNMP 本地环境备忘 佛性写博 Ubuntu 20.04 优化 免费领取咪咕版 Kindle Typecho 迁移到 Hugo 博客迁移到 Hugo GitHub Actions 自动部署 Hexo 脚本 Gridea Hexo Hugo 等 git push 同步到多个仓库 Ubuntu 19.10 优化 Typecho 中英文之间自动加上空格 Ubuntu 配置 EverVim Ubuntu 配置 Guake Ubuntu 配置 Oh-My-Zsh 阿里云镜像改版 备份工具 Duplicati Ubuntu Server snap 安装 Nextcloud Ubuntu Server 安装 Mosh 算命 言论自由 无意义的个性化 「我谈的话题没什么敏感的」 博客换回默认主题 typecho1.2 (18.10.23) 新窗口打开链接 一言 APP 添加 “今日诗词” 官方源 api 让 Firefox 只在新标签页 newtab 显示书签栏 爱子七不责 改革开放 40 年 大民大国 40 年 40 本书
CSS 和 JS 实现博客热力图
Charles Chin · 2024-04-30 · via eallion's Blog

查看实时效果: 👉 统计页

# TL;DR

太长不看,直接看代码 👇

  1. 引入 style.css
  2. 创建 HTML 容器
  3. 引入 heatmap.js

# 前言

五六年前就在 Typecho 上折腾过热力图,以前用 jQuery 折腾挺方便的。
但期间有些博客主题中没有合适的地方放热力图,就放弃了。
最近博客热力图又有热度了,刚好我这个主题可以放在首页,又折腾上了。
期间尝试了几个版本,网上也有非常多类似的库:

优缺点:

  • ECharts.js 不方便控制细节,不方便适配移动端,资源文件比较大;
  • Heat.js 在测试的时候发现了 Cal-Heatmap.js 了;
  • Cal-Heatmap.js 是专门做热力图的,但需要引用多个库和插件。

从 Koobai 大佬发布《 HUGO 折腾随记之热力图 / 段落导航》时,我就说要折腾一个纯 CSS 版的热力图,一直推迟到今天才完成。期间折腾 Twitter Year Progress 时,完成了绘制年度日历小方块,直接用上了。

# 一、JS 构建热力图

# 1. 准备博客数据

在 Hugo 构建时,获取最近一年的文章数据:

// 获取最近一年的文章数据
{{ $pages := where .Site.RegularPages "Date" ">" (now.AddDate -1 0 0) }}
{{ $pages := $pages.Reverse }}
    var blogInfo = {
        "pages": [
            {{ range $index, $element := $pages }}
                {
                    "title": "{{ replace (replace .Title "" "") "" "" }}",
                    "date": "{{ .Date.Format "2006-01-02" }}",
                    "year": "{{ .Date.Format "2006" }}",
                    "month": "{{ .Date.Format "01" }}",
                    "day": "{{ .Date.Format "02" }}",
                    "word_count": "{{ .WordCount }}"
                }{{ if ne (add $index 1) (len $pages) }},{{ end }}
                {{ end }}
        ]
};
// console.log(blogInfo)

这段 JS 会获取到如下示例数据,并存入 blogInfo 中,如果需要 slugsummary 或其他数据,按上面的代码依样画葫芦:

{
    "pages": [
        {
            "title": "CSS 和 JS 实现博客热力图",
            "date": "2024-04-30",
            "year": "2024",
            "month": "04",
            "day": "30",
            "word_count": "685"
        }
    ]
}

# 2. 渲染月份

let monthNames = ['Jan', 'Feb', 'Mar'] 中显示的月份数可以自定义。适配了移动端,常规移动设备显示 6 个月的数据,对于过小的设备,如:iPhone SE / Pixel 4 只显示 5 个月的数据。

let currentDate = new Date();
currentDate.setFullYear(currentDate.getFullYear() - 1);

let startDate;

let monthDiv = document.querySelector('.month');
let monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

if (window.innerWidth <= 375 ) { // iPhone SE
    numMonths = 5;
} else if (window.innerWidth < 768 ) { // iPad Mini
    numMonths = 6;
} else {
    numMonths = 12;
}

let startMonthIndex = (currentDate.getMonth() - (numMonths - 1) + 12) % 12;
for (let i = startMonthIndex; i < startMonthIndex + numMonths; i++) {
    let monthSpan = document.createElement('span');
    let monthIndex = i % 12;
    monthSpan.textContent = monthNames[monthIndex];
    monthDiv.appendChild(monthSpan);
}

动态生成的月份显示在 <div class="month"> 中,所以不管是 TailwindCSS 还是传统 CSS,month 这个 class 不能去掉。

<div class="month heatmap_month"> <!-- 👈 必须要有 [month] -->
    <span>Nov</span>
    <span>Dec</span>
    <span>Jan</span>
    <span>Feb</span>
    <span>Mar</span>
    <span>Apr</span>
</div>

# 3. startDate 之:起始日期从星期一开始渲染

如果单纯地从今天往前渲染 52 个周(一年)的小方块,很简单。不过这样渲染的数据有一个不符合常识的问题,即一年前的今天,并不一定是 星期一,所以在选择热力图的开始日期的时候,需要考虑以 去年今天 所在星期的 星期一 作为起始点。

function getWeekDay(date) {
    const day = date.getDay();
    return day === 0 ? 6 : day - 1;
}

# 4. endDate 之:如果结束日期 今天 超出日历范围

结合第 3 点,如果 今天 的星期数比 去年今天 的星期数小,则会导致渲染 52 个周(一年)的小方块之后,今天今天之后的本周内容渲染不了了,所以需要判断今天的星期数,并追加到年度日历小方块中。

const startDate = getStartDate();
const endDate = new Date();
const weekDay = getWeekDay(startDate);

let currentWeek = createWeek();
container.appendChild(currentWeek);

let currentDate = startDate;
let i = 0;

while (currentDate <= endDate) {
    if (i % 7 === 0 && i !== 0) {
        currentWeek = createWeek();
        container.appendChild(currentWeek);
    }
    i++;
    currentDate.setDate(currentDate.getDate() + 1);
}

# 5. 渲染小方块及 Tooltip

每个小方块以 count 字数显示不同色深的色块,即 CSS heatmap_day_level_num 的样式,count1-1000 1000-2000 2000-3000 3000+ 分为 4 个 level 截断。

我的博客中还渲染了 count post title date 4 数据用于 Tooltip。

  • count data-count 当天文章字数,多篇文章会合并计算
  • post data-post 当天文章数量
  • title data-title 当天文章的标题
  • date data-date 当天的日期 Jan 2, 2006 en-US 格式

当鼠标经过小方块时,以 data-title="" data-count="" data-post="" data-date="" 几个属性的值创建一个当日的 <div class="tooltip"> 标签。

function createDay(date, title, count, post) {
    const day = document.createElement("div");

    day.className = "heatmap_day";

    day.setAttribute("data-title", title);
    day.setAttribute("data-count", count);
    day.setAttribute("data-post", post);
    day.setAttribute("data-date", date);

    day.addEventListener("mouseenter", function () {
        const tooltip = document.createElement("div");
        tooltip.className = "heatmap_tooltip";

        let tooltipContent = "";

        if (post && parseInt(post, 10) !== 0) {
            tooltipContent += '<span class="heatmap_tooltip_post">' + '共 ' + post + ' 篇' + '</span>';
        }

        if (count && parseInt(count, 10) !== 0) {
            tooltipContent += '<span class="heatmap_tooltip_count">' + ' ' + count + ' 字;' + '</span>';
        }

        if (title && parseInt(title, 10) !== 0) {
            tooltipContent += '<span class="heatmap_tooltip_title">' + title + '</span>';
        }

        if (date) {
            tooltipContent += '<span class="heatmap_tooltip_date">' + date + '</span>';
        }

        tooltip.innerHTML = tooltipContent;
        day.appendChild(tooltip);
    });

    day.addEventListener("mouseleave", function () {
        const tooltip = day.querySelector(".heatmap_tooltip");
        if (tooltip) {
            day.removeChild(tooltip);
        }
    });

    if (count == 0) {
        day.classList.add("heatmap_day_level_0");
    } else if (count > 0 && count < 1000) {
        day.classList.add("heatmap_day_level_1");
    } else if (count >= 1000 && count < 2000) {
        day.classList.add("heatmap_day_level_2");
    } else if (count >= 2000 && count < 3000) {
        day.classList.add("heatmap_day_level_3");
    } else {
        day.classList.add("heatmap_day_level_4");
    }

    return day;
}

# 二、完整的 heatmap.js

前面的分解是只一些需要注意的细节,下面是完整的 JS:

// 获取最近一年的文章数据
{{ $pages := where .Site.RegularPages "Date" ">" (now.AddDate -1 0 0) }}
{{ $pages := $pages.Reverse }}
var blogInfo = {
    "pages": [
        {{ range $index, $element := $pages }}
            {
                "title": "{{ replace (replace .Title "" "") "" "" }}",
                "date": "{{ .Date.Format "2006-01-02" }}",
                "year": "{{ .Date.Format "2006" }}",
                "month": "{{ .Date.Format "01" }}",
                "day": "{{ .Date.Format "02" }}",
                "word_count": "{{ .WordCount }}"
            }{{ if ne (add $index 1) (len $pages) }},{{ end }}
            {{ end }}
    ]
};
// console.log(blogInfo)

let currentDate = new Date();
currentDate.setFullYear(currentDate.getFullYear() - 1);

let startDate;

let monthDiv = document.querySelector('.month');
let monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

if (window.innerWidth < 768) {
    numMonths = 6;
} else {
    numMonths = 12;
}

let startMonthIndex = (currentDate.getMonth() - (numMonths - 1) + 12) % 12;
for (let i = startMonthIndex; i < startMonthIndex + numMonths; i++) {
    let monthSpan = document.createElement('span');
    let monthIndex = i % 12;
    monthSpan.textContent = monthNames[monthIndex];
    monthDiv.appendChild(monthSpan);
}

function getStartDate() {
    const today = new Date();

    if (window.innerWidth < 768) {
        numMonths = 6;
    } else {
        numMonths = 12;
    }

    const startDate = new Date(today.getFullYear(), today.getMonth() - numMonths + 1, 1, today.getHours(), today.getMinutes(), today.getSeconds());

    while (startDate.getDay() !== 1) {
        startDate.setDate(startDate.getDate() + 1);
    }

    return startDate;
}

function getWeekDay(date) {
    const day = date.getDay();
    return day === 0 ? 6 : day - 1;
}

function createDay(date, title, count, post) {
    const day = document.createElement("div");

    day.className = "heatmap_day";

    day.setAttribute("data-title", title);
    day.setAttribute("data-count", count);
    day.setAttribute("data-post", post);
    day.setAttribute("data-date", date);

    day.addEventListener("mouseenter", function () {
        const tooltip = document.createElement("div");
        tooltip.className = "heatmap_tooltip";

        let tooltipContent = "";

        if (post && parseInt(post, 10) !== 0) {
            tooltipContent += '<span class="heatmap_tooltip_post">' + '共 ' + post + ' 篇' + '</span>';
        }

        if (count && parseInt(count, 10) !== 0) {
            tooltipContent += '<span class="heatmap_tooltip_count">' + ' ' + count + ' 字;' + '</span>';
        }

        if (title && parseInt(title, 10) !== 0) {
            tooltipContent += '<span class="heatmap_tooltip_title">《' + title + '》</span>';
        }

        if (date) {
            tooltipContent += '<span class="heatmap_tooltip_date">' + date + '</span>';
        }

        tooltip.innerHTML = tooltipContent;
        day.appendChild(tooltip);
    });

    day.addEventListener("mouseleave", function () {
        const tooltip = day.querySelector(".heatmap_tooltip");
        if (tooltip) {
            day.removeChild(tooltip);
        }
    });

    if (count == 0 ) {
        day.classList.add("heatmap_day_level_0");
    } else if (count > 0 && count < 1000) {
        day.classList.add("heatmap_day_level_1");
    } else if (count >= 1000 && count < 2000) {
        day.classList.add("heatmap_day_level_2");
    } else if (count >= 2000 && count < 3000) {
        day.classList.add("heatmap_day_level_3");
    } else {
        day.classList.add("heatmap_day_level_4");
    }

    return day;
}

function createWeek() {
    const week = document.createElement('div');
    week.className = 'heatmap_week';
    return week;
}

function createHeatmap() {
    const container = document.getElementById('heatmap');
    const startDate = getStartDate();
    const endDate = new Date();
    const weekDay = getWeekDay(startDate);

    let currentWeek = createWeek();
    container.appendChild(currentWeek);

    let currentDate = startDate;
    let i = 0;

    while (currentDate <= endDate) {
        if (i % 7 === 0 && i !== 0) {
            currentWeek = createWeek();
            container.appendChild(currentWeek);
        }

        const dateString = `${currentDate.getFullYear()}-${("0" + (currentDate.getMonth()+1)).slice(-2)}-${("0" + (currentDate.getDate())).slice(-2)}`;

        const articleDataList = blogInfo.pages.filter(page => page.date === dateString);

        if (articleDataList.length > 0) {
            const titles = articleDataList.map(data => data.title);
            const title = titles.map(t => `${t}`).join('<br />');

            let count = 0;
            let post = articleDataList.length;

            articleDataList.forEach(data => {
                count += parseInt(data.word_count, 10);
            });

            const formattedDate = formatDate(currentDate);
            const day = createDay(formattedDate, title, count, post);
            currentWeek.appendChild(day);
        } else {
            const formattedDate = formatDate(currentDate);
            const day = createDay(formattedDate, '', '0', '0');
            currentWeek.appendChild(day);
        }

        i++;
        currentDate.setDate(currentDate.getDate() + 1);
    }
}

function formatDate(date) {
    const options = { month: 'short', day: 'numeric', year: 'numeric' };
    return date.toLocaleDateString('en-US', options);
}

createHeatmap();

# 三、HTML DIV 容器

准备 HTML 容器,用于渲染 Heatmap,我博客用的是 TailwindCSS,为了写文章,已转成传统 CSS 样式,相当于用 CSS 重新实现了一遍。
全部使用 Flex 排版,为了适配移动端,用 JS 检测屏幕宽度动态生成月份和年度日历小方块。做了 2 个截断,一是个 iPhone SE 的 375 宽度和 iPad Mini 的 768 宽度,宽度截断在后面的 JS 中可以看到。

<div class="heatmap_container"> <!-- 全部用 Flex 排版 -->
    <div class="heatmap_content">
        <div class="heatmap_week">
            <span>Mon</span>
            <span>&nbsp;</span> <!-- 不需要显示的星期用空格表示 -->
            <span>Wed</span>
            <span>&nbsp;</span>
            <span>Fri</span>
            <span>&nbsp;</span>
            <span>Sun</span>
        </div>
        <div class="heatmap_main">
            <div class="month heatmap_month">
                <!-- js 检测屏幕宽度动态生成月份 -->
            </div>
            <div id="heatmap" class="heatmap">
                <!-- js 检测屏幕宽度动态生成年度日历小方块 -->
            </div>
        </div>
    </div>
    <div class="heatmap_footer">
        <div class="heatmap_less">Less</div>
        <div class="heatmap_level">
            <span class="heatmap_level_item heatmap_level_0"></span>
            <span class="heatmap_level_item heatmap_level_1"></span>
            <span class="heatmap_level_item heatmap_level_2"></span>
            <span class="heatmap_level_item heatmap_level_3"></span>
            <span class="heatmap_level_item heatmap_level_4"></span>
        </div>
        <div class="heatmap_more">More</div>
    </div>
</div>

# 四、传统 style.css

CSS 样式仿照的是 GitHub 的配色,Dark mode 是 GitHub Dimmed 的配色。

:root {
    /* GitHub Light Color */
    --ht-main: #334155;
    --ht-day-bg: #ebedf0;
    --ht-tooltip: #24292f;
    --ht-tooltip-bg: #fff;
    --ht-lv-0: #ebedf0;
    --ht-lv-1: #9be9a8;
    --ht-lv-2: #40c463;
    --ht-lv-3: #30a14e;
    --ht-lv-4: #216e39;
}

[data-theme="dark"] {
    /* GitHub Dark Dimmed Color */
    --ht-main: #94a3b8;
    --ht-day-bg: #161b22;
    --ht-tooltip: #24292f;
    --ht-tooltip-bg: #fff;
    --ht-lv-0: #161b22;
    --ht-lv-1: #0e4429;
    --ht-lv-2: #006d32;
    --ht-lv-3: #26a641;
    --ht-lv-4: #39d353;
}

.heatmap_container {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    font-size: 10px;
    line-height: 12px;
    color: var(--ht-main);
}

.heatmap_content {
    display: flex;
    flex-direction: row;
    align-items: flex-end
}

.heatmap_week {
    display: flex;
    margin-top: 0.25rem;
    margin-right: 0.25rem;
    flex-direction: column;
    justify-content: flex-end;
    align-items: flex-end;
    text-align: right
}

.heatmap_main {
    display: flex;
    flex-direction: column
}

.heatmap_month {
    display: flex;
    margin-top: 0.25rem;
    margin-right: 0.25rem;
    flex-direction: column;
    justify-content: space-around;
    align-items: flex-end;
    text-align: right;
}

.heatmap {
    display: flex;
    flex-direction: row;
    height: 84px;
}

.heatmap_footer {
    display: flex;
    margin-top: 0.5rem;
    align-items: center
}

.heatmap_level {
    display: flex;
    gap: 2px;
    margin: 0 0.25rem;
    flex-direction: row;
    align-items: center;
    width: max-content;
    height: 10px
}

.heatmap_level_item {
    display: block;
    border-radius: 0.125rem;
    width: 10px;
    height: 10px;
}

.heatmap_level_0 {
    background: var(--ht-lv-0);
}

.heatmap_level_1 {
    background: var(--ht-lv-1);
}

.heatmap_level_2 {
    background: var(--ht-lv-2);
}

.heatmap_level_3 {
    background: var(--ht-lv-3);
}

.heatmap_level_4 {
    background: var(--ht-lv-4);
}

.heatmap_week {
    display: flex;
    flex-direction: column;
}

.heatmap_day {
    width: 10px;
    height: 10px;
    background-color: var(--ht-day-bg);
    margin: 1px;
    border-radius: 2px;
    display: inline-block;
    position: relative;
}

.heatmap_tooltip {
    position: absolute;
    bottom: 12px;
    left: 50%;
    width: max-content;
    color: var(--ht-tooltip);
    background-color: var(--ht-tooltip-bg);
    font-size: 12px;
    line-height: 16px;
    padding: 8px;
    border-radius: 3px;
    white-space: pre-wrap;
    opacity: 1;
    transition: 0.3s;
    z-index: 1000;
    text-align: right;
    transform: translateX(-50%);
}

.heatmap_tooltip_count,
.heatmap_tooltip_post {
    display: inline-block;
}

.heatmap_tooltip_title,
.heatmap_tooltip_date {
    display: block;
}

.heatmap_tooltip_date {
    margin: 0 0.25rem;
}

.heatmap_day_level_0 {
    background-color: var(--ht-lv-0);
}

.heatmap_day_level_1 {
    background-color: var(--ht-lv-1);
}

.heatmap_day_level_2 {
    background-color: var(--ht-lv-2);
}

.heatmap_day_level_3 {
    background-color: var(--ht-lv-3);
}

.heatmap_day_level_4 {
    background-color: var(--ht-lv-4);
}

# 五、TailwindCSS 样式

<div class="flex flex-col items-end text-[10px] leading-[12px] text-neutral-700 dark:text-neutral-400">
    <div class="flex flex-row items-end">
        <div class="flex flex-col justify-end items-end mr-1 mt-1 text-right">
            <span>Mon</span>
            <span>&nbsp;</span>
            <span>Wed</span>
            <span>&nbsp;</span>
            <span>Fri</span>
            <span>&nbsp;</span>
            <span>Sun</span>
        </div>
        <div class="heatmap flex flex-col">
            <div class="month mb-1 flex justify-around">
            </div>
            <div class="h-[84px]">
                <div id="heatmap" class="flex flex-row"></div>
            </div>
        </div>
    </div>
    <div class="flex mt-2 items-center">
        <span class="">Less</span>
        <div class="flex flex-row items-center gap-[2px] w-max h-[10px] mx-1">
            <span class="block w-[10px] h-[10px] rounded-sm bg-[#ebedf0] dark:bg-[#161b22]"></span>
            <span class="block w-[10px] h-[10px] rounded-sm bg-[#9be9a8] dark:bg-[#0e4429]"></span>
            <span class="block w-[10px] h-[10px] rounded-sm bg-[#40c463] dark:bg-[#006d32]"></span>
            <span class="block w-[10px] h-[10px] rounded-sm bg-[#30a14e] dark:bg-[#26a641]"></span>
            <span class="block w-[10px] h-[10px] rounded-sm bg-[#216e39] dark:bg-[#39d353]"></span>
        </div>
        <span class="">More</span>
    </div>
</div>