GitHub
加载中...
加载GitHub贡献图...
✨ 欢迎来到桃源笔记
这里是记录生活、分享技术、探索世界的小天地。愿你在忙碌的生活中,也能找到属于自己的那片桃源~
标签
安全 宝库 笔记 编程实践 博客 触摸事件 低碳生活 调试 都市减压 独处 个人成长 个性化教育 更新日志 工具 工业4.0 工作生活平衡 公告 构建 孤独 观察 航天技术 合成生物学 环保 火星任务 基因编辑 计算器 技能 技术创新 加密货币 焦虑 教师生涯 教育 开源 科技趋势 科技与生活 可持续生活 量子计算 灵活办公 零浪费 浏览器API 留白艺术 慢生活哲学 命令行工具 内心成长 评论系统 前端 区块链 去中心化 人工智能 人文 人性思考 商业航天 商业应用 生活 生活方式 生活平衡 生活哲学 生态友好 生物技术 师德楷模 时间管理 实验室 实用方法 书单 数字化 数字极简主义 数字经济 数字孪生 数字伦理 数字转型 随笔 太空探索 太空殖民 体验优化 天津宝坻 推荐 未来工作 未来经济 未来科技 未来学习 未来展望 物联网 咸鱼之王 乡村教育 小工具 效率 效率工具 协议 心理健康 心理健康工具 心灵治愈 性能优化 虚拟现实 医疗创新 移动端 游戏 语文教学 元宇宙 远程工作 远程医疗 阅读 增强现实 正念 知识管理 智慧生活 智能制造 专注力 自我关怀 自我疗愈 自我探索 自我提升 Astro Canvas GitHub Obsidian Pagefind PKM Python RSS Rust Svelte Umami Web3
1070 字
5 分钟
周末做了个小玩意:GitHub 贡献图生成器
起因
周末闲着没事,想给博客加个”代码贡献统计”的页面。GitHub 自带的贡献图挺好看,但样式固定,不能自定义。
于是花了一个下午,用 Canvas API 撸了个生成器。
效果预览
支持的功能:
- ✅ 自定义颜色主题(GitHub 绿、蓝色、紫色等)
- ✅ 调整格子大小和间距
- ✅ 显示/隐藏月份标签
- ✅ 导出为 PNG 或 SVG
- ✅ 响应式设计,移动端友好
技术实现
数据结构
首先定义贡献数据的结构:
interface Contribution { date: string; // 'YYYY-MM-DD' count: number; // 贡献次数 level: 0 | 1 | 2 | 3 | 4; // 颜色等级}
interface ContributionData { contributions: Contribution[]; totalCount: number; startDate: string; endDate: string;}Canvas 绘制
核心绘制逻辑:
class ContributionGraph { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private cellSize = 10; private cellGap = 2; private colors = { 0: "#161b22", // 无贡献 1: "#0e4429", // 1-3 次 2: "#006d32", // 4-6 次 3: "#26a641", // 7-9 次 4: "#39d353", // 10+ 次 };
constructor(canvas: HTMLCanvasElement) { this.canvas = canvas; this.ctx = canvas.getContext("2d")!; }
draw(data: ContributionData) { const weeks = this.groupByWeek(data.contributions); const width = weeks.length * (this.cellSize + this.cellGap); const height = 7 * (this.cellSize + this.cellGap);
this.canvas.width = width; this.canvas.height = height;
weeks.forEach((week, weekIndex) => { week.forEach((day, dayIndex) => { const x = weekIndex * (this.cellSize + this.cellGap); const y = dayIndex * (this.cellSize + this.cellGap);
this.ctx.fillStyle = this.colors[day.level]; this.ctx.fillRect(x, y, this.cellSize, this.cellSize); }); }); }
private groupByWeek(contributions: Contribution[]) { // 按周分组逻辑 const weeks: Contribution[][] = []; let currentWeek: Contribution[] = [];
contributions.forEach((contrib, index) => { const date = new Date(contrib.date); const dayOfWeek = date.getDay();
if (dayOfWeek === 0 && currentWeek.length > 0) { weeks.push(currentWeek); currentWeek = []; }
currentWeek.push(contrib); });
if (currentWeek.length > 0) { weeks.push(currentWeek); }
return weeks; }}交互功能
添加鼠标悬停提示:
canvas.addEventListener("mousemove", (e) => { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top;
const weekIndex = Math.floor(x / (cellSize + cellGap)); const dayIndex = Math.floor(y / (cellSize + cellGap));
const contribution = weeks[weekIndex]?.[dayIndex];
if (contribution) { showTooltip(e.clientX, e.clientY, { date: contribution.date, count: contribution.count, }); } else { hideTooltip(); }});导出功能
导出为 PNG:
function exportToPNG() { const link = document.createElement("a"); link.download = "contribution-graph.png"; link.href = canvas.toDataURL("image/png"); link.click();}导出为 SVG(更清晰):
function exportToSVG(data: ContributionData) { const weeks = groupByWeek(data.contributions); const width = weeks.length * (cellSize + cellGap); const height = 7 * (cellSize + cellGap);
let svg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">`;
weeks.forEach((week, weekIndex) => { week.forEach((day, dayIndex) => { const x = weekIndex * (cellSize + cellGap); const y = dayIndex * (cellSize + cellGap); const color = colors[day.level];
svg += `<rect x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" fill="${color}" />`; }); });
svg += "</svg>";
const blob = new Blob([svg], { type: "image/svg+xml" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.download = "contribution-graph.svg"; link.href = url; link.click();}优化点
1. 性能优化
最开始每次鼠标移动都重绘整个 Canvas,很卡。优化后只在必要时重绘:
let lastHoveredCell: [number, number] | null = null;
canvas.addEventListener("mousemove", (e) => { const cell = getCellAtPosition(e.clientX, e.clientY);
if (!isSameCell(cell, lastHoveredCell)) { redrawHighlight(cell); lastHoveredCell = cell; }});2. 响应式设计
根据容器宽度自动调整格子大小:
function calculateCellSize(containerWidth: number, weeks: number) { const availableWidth = containerWidth - 40; // 留出边距 const totalGaps = (weeks - 1) * cellGap; return Math.floor((availableWidth - totalGaps) / weeks);}3. 主题切换
支持多种配色方案:
const themes = { github: { 0: "#161b22", 1: "#0e4429", 2: "#006d32", 3: "#26a641", 4: "#39d353", }, blue: { 0: "#161b22", 1: "#0a3069", 2: "#0969da", 3: "#54aeff", 4: "#79c0ff", }, purple: { 0: "#161b22", 1: "#3d1f47", 2: "#6e40aa", 3: "#a06cd5", 4: "#d896ff", },};使用方法
安装
npm install github-contribution-graph基础用法
import { ContributionGraph } from "github-contribution-graph";
const canvas = document.getElementById("graph") as HTMLCanvasElement;const graph = new ContributionGraph(canvas);
// 从 GitHub API 获取数据const data = await fetchGitHubContributions("username");
graph.draw(data);自定义配置
const graph = new ContributionGraph(canvas, { cellSize: 12, cellGap: 3, theme: "blue", showMonthLabels: true, showWeekdayLabels: true,});遇到的坑
坑1:日期计算
JavaScript 的 Date 对象有很多坑,特别是时区问题。最后用了 date-fns 库:
import { startOfWeek, endOfWeek, eachDayOfInterval } from "date-fns";
const days = eachDayOfInterval({ start: startOfWeek(startDate), end: endOfWeek(endDate),});坑2:Canvas 模糊
在高 DPI 屏幕上,Canvas 会模糊。解决方案:
const dpr = window.devicePixelRatio || 1;canvas.width = width * dpr;canvas.height = height * dpr;canvas.style.width = `${width}px`;canvas.style.height = `${height}px`;ctx.scale(dpr, dpr);坑3:SVG 导出中文乱码
SVG 导出时,中文会乱码。需要指定编码:
const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8",});后续计划
- 支持更多主题(暗黑模式、彩虹色等)
- 添加动画效果(渐入、波浪等)
- 支持自定义数据源(不只是 GitHub)
- 做成 Web Component,方便集成
- 发布到 npm
开源地址
代码已开源:github.com/example/contribution-graph
欢迎 star 和 PR!
在线体验
做了个在线 Demo:contribution-graph.demo.com
可以直接输入 GitHub 用户名生成贡献图。
技术栈:
- TypeScript
- Canvas API
- date-fns
- Vite
开发时间:
- 核心功能:4 小时
- 优化和测试:2 小时
- 文档和 Demo:2 小时
总共一个周末搞定,还是挺有成就感的 😄
部分信息可能已经过时




