Skip to content

Instantly share code, notes, and snippets.

@bczhc
Created January 12, 2026 05:15
Show Gist options
  • Select an option

  • Save bczhc/aaa8d936691b0bf13d5221aa73030a2b to your computer and use it in GitHub Desktop.

Select an option

Save bczhc/aaa8d936691b0bf13d5221aa73030a2b to your computer and use it in GitHub Desktop.
仿GitHub贡献日期热力图 HTML
<!DOCTYPE html>
<!-- Co-created with Gemini -->
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>贡献热力图 - 多年份支持</title>
<style>
:root {
--cell-size: 11px;
--cell-gap: 3px;
--active-color: #39d353;
--empty-color: #ebedf0;
--text-color: #767676;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
padding: 60px 50px;
background: white;
display: flex;
flex-direction: column;
align-items: center;
/* 增加组件之间的垂直间距 */
gap: 100px;
}
.heatmap-wrapper {
display: flex;
flex-direction: column;
align-items: flex-start;
/* 为每个组件添加底部边距作为双重保障 */
padding-bottom: 40px;
}
.year-title {
font-size: 20px;
font-weight: 600;
color: #24292f;
width: 100%;
padding-bottom: 8px;
}
.graph-container {
display: flex;
flex-direction: column;
position: relative;
}
.months-container {
display: flex;
margin-left: 35px;
height: 20px;
font-size: 12px;
color: var(--text-color);
position: relative;
margin-bottom: 4px;
}
.month-label {
position: absolute;
white-space: nowrap;
}
.main-grid {
display: flex;
gap: 8px;
}
.days {
display: grid;
grid-template-rows: repeat(7, var(--cell-size));
gap: var(--cell-gap);
font-size: 10px;
color: var(--text-color);
padding-top: 2px;
}
.days span {
height: var(--cell-size);
line-height: var(--cell-size);
display: flex;
align-items: center;
}
.grid {
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(7, var(--cell-size));
gap: var(--cell-gap);
}
.cell {
width: var(--cell-size);
height: var(--cell-size);
border-radius: 2px;
background-color: var(--empty-color);
cursor: default;
}
#custom-tooltip {
position: fixed;
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 5px 10px;
border-radius: 4px;
font-size: 11px;
pointer-events: none;
display: none;
z-index: 1000;
white-space: nowrap;
}
</style>
</head>
<body>
<!-- 用于渲染所有年份图表的容器 -->
<div id="app"></div>
<div id="custom-tooltip"></div>
<script>
async function getRemoteTxt(url) {
const [response] = await Promise.all([fetch(url)]);
if (!response.ok) throw new Error('网络请求失败');
return await response.text();
}
/**
* 有级映射函数:根据预设区间返回颜色
* @param {number} value 当前数值
*/
function getColorForValue(value) {
// 0 值处理
if (value === 0) return 'var(--empty-color)';
// 1 - 1000:最浅绿
if (value <= 1000) {
return '#9be9a8'; // Level 1
}
// 1001 - 2000:浅绿
if (value <= 2000) {
return '#40c463'; // Level 2
}
// 2001 - 5000:中绿
if (value <= 5000) {
return '#30a14e'; // Level 3
}
// 5000+:深绿
return '#216e39'; // Level 4
}
/**
* 解析原始字符串数据 (支持 YYYYMMDD|Value 格式)
*/
function parseRawData(str) {
const lines = str.trim().split(/\s+/);
const dataByYear = {};
lines.forEach(line => {
const parts = line.split('|');
const datePart = parts[0];
const valuePart = parseInt(parts[1] || 0, 10);
if (datePart.length === 8) {
const year = datePart.substring(0, 4);
const month = datePart.substring(4, 6);
const day = datePart.substring(6, 8);
const formattedDate = `${year}-${month}-${day}`;
if (!dataByYear[year]) dataByYear[year] = {};
dataByYear[year][formattedDate] = valuePart;
}
});
return dataByYear;
}
/**
* 渲染单个热力图组件
*/
function renderHeatmap(year, dateValues) {
const app = document.getElementById('app');
const wrapper = document.createElement('div');
wrapper.className = 'heatmap-wrapper';
const title = document.createElement('div');
title.className = 'year-title';
title.innerText = year;
wrapper.appendChild(title);
wrapper.innerHTML += `
<div class="graph-container">
<div id="months-${year}" class="months-container"></div>
<div class="main-grid">
<div id="days-${year}" class="days"></div>
<div id="grid-${year}" class="grid"></div>
</div>
</div>
`;
app.appendChild(wrapper);
const grid = document.getElementById(`grid-${year}`);
const monthsRow = document.getElementById(`months-${year}`);
const daysColumn = document.getElementById(`days-${year}`);
const tooltip = document.getElementById('custom-tooltip');
const firstDayOfYear = new Date(`${year}-01-01`);
const startDayIndex = firstDayOfYear.getDay();
const dayLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
for (let i = 0; i < 7; i++) {
const span = document.createElement('span');
if (i === 1 || i === 3 || i === 5) {
span.innerText = dayLabels[(startDayIndex + i) % 7];
}
daysColumn.appendChild(span);
}
const isLeap = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
const daysInYear = isLeap ? 366 : 365;
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
// 获取该年份的非0最小值和最大值
const values = Object.values(dateValues).filter(v => v > 0);
const maxValue = values.length > 0 ? Math.max(...values) : 0;
const minValue = values.length > 0 ? Math.min(...values) : 0;
for (let i = 0; i < daysInYear; i++) {
const currentDate = new Date(firstDayOfYear);
currentDate.setDate(firstDayOfYear.getDate() + i);
const cell = document.createElement('div');
cell.className = 'cell';
const y = currentDate.getFullYear();
const m = String(currentDate.getMonth() + 1).padStart(2, '0');
const d = String(currentDate.getDate()).padStart(2, '0');
const dateStr = `${y}-${m}-${d}`;
const value = dateValues[dateStr] || 0;
cell.dataset.date = dateStr;
cell.dataset.value = value;
// 应用无级颜色映射
cell.style.backgroundColor = getColorForValue(value, minValue, maxValue);
cell.addEventListener('mouseenter', () => {
tooltip.innerText = `${cell.dataset.date} : ${cell.dataset.value}`;
tooltip.style.display = 'block';
});
cell.addEventListener('mousemove', (e) => {
tooltip.style.left = (e.clientX + 10) + 'px';
tooltip.style.top = (e.clientY - 30) + 'px';
});
cell.addEventListener('mouseleave', () => tooltip.style.display = 'none');
grid.appendChild(cell);
const currentWeekColumn = Math.floor(i / 7);
if (currentDate.getDate() === 1) {
const currentMonth = currentDate.getMonth();
const monthLabel = document.createElement('span');
monthLabel.className = 'month-label';
monthLabel.innerText = monthNames[currentMonth];
monthLabel.style.left = `${currentWeekColumn * (11 + 3)}px`;
monthsRow.appendChild(monthLabel);
}
}
}
function processData(rawDataStr) {
const dataByYear = parseRawData(rawDataStr);
Object.keys(dataByYear).sort((a, b) => b - a).forEach(year => {
renderHeatmap(year, dataByYear[year]);
});
}
// processData(`
// 20141103|286
// 20141104|132
// 20141105|187
// 20141106|201
// 20141107|263
// 20141108|173
// 20141109|161
// 20141110|257
// 20141111|158
// 20141113|243
// 20200519|50
// 20200520|100
// 20200521|150
// 20200522|200
// 20200524|250
// `);
getRemoteTxt("a.txt").then(x => processData(x));
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment