Created
January 12, 2026 05:15
-
-
Save bczhc/aaa8d936691b0bf13d5221aa73030a2b to your computer and use it in GitHub Desktop.
仿GitHub贡献日期热力图 HTML
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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