ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用GitHub热力图打造阅读打卡系统:习惯可视化实践指南

用GitHub热力图打造阅读打卡系统:习惯可视化实践指南 GitHub Heatmap for Reading核心想法一句话就能说清楚把你每天阅读的时长、页数或完成情况按照 GitHub 主页那套“绿点矩阵”展示出来。它解决的实际问题不是“我怎么记录读书”而是“我怎么让阅读的连续性变得一眼可见”。很多人以为这类工具的价值是好看真正跑起来之后你会发现它最有用的部分是让你无法回避空白格子哪一天没读哪一周在偷懒全都摊在图上。这篇文章适合正在做个人数据可视化项目的开发者也适合想给自己建一个阅读打卡看板的普通人。前者可以看完整实现思路后者可以直接按落地顺序搭一套。先说我的总体判断这种项目对机器配置基本没有要求对数据规范和定时任务的要求反而更高。你最容易踩的坑不在绘图代码而在“数据源不稳定”和“时间口径不统一”。1. 先想清楚它复刻的不是图形是“习惯可视化”这套逻辑1.1 GitHub 绿点矩阵到底是什么GitHub 主页那个热力图本质上是一张 7 行、约 53 列的网格。列是一周行是周一到周日每一天对应一个格子。格子颜色深浅由当天行为数量决定没有行为就是浅灰色行为越多颜色越深。这套图形真正厉害的地方不是配色而是它能同时表达三件事单日强度这一天做了多少。周期性节奏周末有没有集中爆发工作日是不是断断续续。连续记录连续多少天没有空窗。放在阅读场景里这套逻辑非常合适。很多阅读 App 的年度报告只告诉你“今年读了多少本、多少小时”那是总结不是过程。热力图把过程也画出来了你是月初猛读三天然后歇两周还是每天稳定读半小时一眼就能分辨。1.2 它和阅读 App 年度报告的区别微信读书、豆瓣、Kindle 都有自己的统计页面但它们的问题都一样数据被锁在各自平台里且统计口径是平台定的。阅读热力图类项目的价值是让你自己决定口径并且把多个来源的数据合并到同一张图里。你可以把微信读书的时长、Kindle 的页数、纸质书的手动记录全部合并成一份数据集。这个自由度是任何单一 App 都给不了的。另外一个区别是所有权。自己做的热力图数据是本地 JSON 或私有仓库里的文件不会因为平台改版、功能下架而消失。你维护的是一个长期可用的个人记录系统不是一个临时报表。1.3 先定统计口径时长、页数还是完成状态这是整个项目里最关键、也最容易被跳过的一步。动手写代码之前你必须先回答一个问题什么才算“某一天读了书”常见口径有三种阅读时长按分钟统计适合微信读书这类自带计时器的数据源。阅读页数按页统计适合 Kindle、纸质书手动输入。完成状态读完一本书算一次适合只关注“读完”而不是“读了多久”的人。我建议你用一个统一的主口径其他口径作为附加字段。不要今天按时长明天按页数后天又改成“读满 30 分钟才算”。口径一换颜色阈值要重调历史数据也要重新映射非常麻烦。还有一个小问题要提前想好读 5 分钟也算一天吗如果算你的热力图会非常满但参考价值很低。如果不算阈值设在哪里需要在后面画图前一起定掉。2. 数据源是第一步热力图只是最后一步2.1 常见的数据来源热力图能不能长期跑下去完全取决于数据源能不能稳定提供数据。常见的阅读来源大概有下面几类数据源数据形态获取难度稳定性微信读书阅读时长、书籍信息中需要登录态接口可能随时间变化豆瓣读书想读、在读、读过低有公开页面较稳定Kindle标注、笔记、读完状态中需解析本地文件很稳定纸质书手动记录自建 JSON/CSV低完全可控自己开发的阅读进度表任意字段低完全可控我的建议是优先选一个确定性最高的来源作为基底。对大多数人来说手动维护一个 JSON 文件虽然笨但最稳定。等这个链路跑通了再考虑接入微信读书或豆瓣。2.2 统一数据模型不管数据从哪来最终都要转成一套统一的结构。推荐用按日期聚合的记录而不是一条条原始流水。比如{ date: 2025-01-01, minutes: 45, pages: 30, finished: 0 }date是本地日期格式固定为YYYY-MM-DD。minutes、pages是当天累计值。finished表示当天是否读完一本书1 或 0。把原始流水聚合成这种结构之后画图就非常简单拿着日期查值填格子就行。后面加新的数据源也只是在生成聚合文件时多一个合并步骤。2.3 清洗要点时区、重复记录、跨天真实数据不会像示例这么干净常见三个坑要先处理。第一个是时区。你记录阅读动作可能发生在晚上十一点半如果按 UTC 存时间日期会跳到第二天。个人项目我建议统一用本地日期字符串不要存时间戳省去大量换算。第二个是重复记录。微信读书导出、豆瓣数据抓取都可能产生重复条目。合并时一定要按“日期 来源”做去重否则某一天的值会翻倍颜色等级直接失真。第三个是跨天阅读。晚上 23:50 读到第二天 00:20这 30 分钟算哪一天不同人处理方式不同。我习惯按开始日期归属也就是不论读多久都算到开始阅读的那一天。这样处理简单和直觉一致。3. 热力图核心实现把数据变成绿点矩阵3.1 年份网格的基本算法画热力图之前先把数据结构想清楚。我们需要一个按日期查值的映射然后生成一个包含 53 周、每周 7 天的网格。下面这段 Python 示例演示了核心思路import datetime import json def build_calendar(year, records): # 先把记录转成 date - value 的映射 day_map {} for r in records: day_map[r[date]] r.get(minutes, 0) start datetime.date(year, 1, 1) # 对齐到往前最近的周日保持网格完整 while start.weekday() ! 6: start - datetime.timedelta(days1) end datetime.date(year, 12, 31) weeks [] d start while d end: week [] for _ in range(7): iso d.isoformat() week.append(day_map.get(iso, 0)) d datetime.timedelta(days1) weeks.append(week) return weeks这段代码的关键在于对齐起始日。如果不把 1 月 1 日对齐到周日第一周只有几天整个网格就会错位。GitHub 自己的图也有这个特点年初和年末会带前后几天的灰色格子用来保证整体布局完整。3.2 颜色分级和阈值设置拿到每个格子的值之后不能直接画颜色还要做分级。GitHub 常用的意思是 5 档0、低、中、高、极高对应从浅到深的绿色。很多初学者直接套 GitHub 的固定阈值比如“1 到 3 次算低4 到 6 次算中”。但阅读数据分布和 GitHub 提交数据不一样。有人一天读 10 分钟有人一天读 2 小时直接套固定阈值会让热力图大部分格子都一个颜色。更好的做法是按照真实数据分布来分位。我的建议是等级判断条件对应感受空值为 0完全没读低大于 0且低于中位数读了一点中中位数到 75 分位正常阅读高75 分位到 90 分位很投入极高超过 90 分位爆发式阅读示例中的“中位数、75 分位”都可以从你已经聚合好的数据里计算出来。这样不管你是轻度阅读还是重度阅读热力图都能自然拉开层次。3.3 连续天数怎么算除了颜色很多阅读热力图还会在顶部显示一个数字连续阅读多少天。这个功能本质上是 streak 计算。算法不复杂把记录按日期排序从早到晚遍历如果当天值大于 0连续天数加一否则清零。但这里有一个值得提前决策的点是否把“今天”算作连续。如果今天还没读书但昨天已经连续 20 天你希望图表显示 20 还是 21我建议显示“截至昨天”的连续天数并且明确标注统计截止日期避免数据失真。还有一个容易被忽略的问题阈值是什么。如果只读过 1 页也算连续那这个连续数字会非常漂亮但没有意义。我一般会把“有效阅读日”的阈值设在 10 分钟或 5 页以上和前面统计口径保持一致。3.4 渲染方式生成图片还是写网页数据模型和算法有了剩下就是怎么展示。常见三种方案方案优点缺点生成 SVG/PNG 图片提交到仓库嵌入 README展示简单README 直接可见每次更新都要重新生成并提交静态 HTML 页面用 JavaScript 前端画图交互强可以加悬浮提示需要部署到 GitHub Pages 或自己的网站后端接口动态返回图片可实时更新个人项目里过度设计维护成本高如果是新手我的建议是先走第一个方案脚本生成图片提交到仓库README 里引用图片。这样做链路最短出问题也容易排查。等确定要长期维护再考虑升级成 GitHub Pages 页面。4. 完整落地顺序先本地再自动化4.1 先用假数据跑通不要一上来就接真实数据。第一次测试应该用一份假 JSON比如两周以内的模拟记录把生成脚本跑通。要验证的点有三个脚本能正常启动输出网格数据。颜色分级结果符合预期。最终生成的图片或 HTML 能在浏览器里正常打开。这一阶段不要调参不要纠结颜色好不好看哪怕只有两种颜色也算通过。重点是确认整条链路没有断点。4.2 再接入真实数据假数据跑通后把真实阅读记录填进去。第一次接入时建议只接一个来源并且从最近一周开始不要一上来就导几年的历史数据。历史数据有一个隐蔽问题年份跨度大早期数据质量可能很低。比如 Kindle 里多年前的标注可能没有准确日期或者日期格式混乱。如果这些脏数据直接进入聚合脚本会导致某几天出现异常峰值颜色分级整体被拉偏。所以先接最近数据确认热力图正常之后再逐步把历史数据按周补进去。每补一批就检查一次颜色分布是否合理。4.3 用 GitHub Actions 定时更新如果每天都手动跑一次脚本坚持不了几天。阅读热力图要长期有效最好加一个定时任务。最常见的方式是 GitHub Actions。下面是一个通用 workflow 示例name: update-reading-heatmap on: schedule: - cron: 0 22 * * * workflow_dispatch: permissions: contents: write jobs: update: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv5 with: python-version: 3.12 - name: Run generate script run: python generate_heatmap.py - name: Commit and push run: | git config user.name github-actions[bot] git config user.email 41898282github-actions[bot]users.noreply.github.com git add . git diff --quiet git diff --cached --quiet || git commit -m update reading heatmap git push这个示例里有两个关键点。第一是cron时区。GitHub Actions 的 cron 默认按 UTC 执行如果你希望每天凌晨按北京时间更新要把时间往前推 8 小时比如用0 22 * * *。如果不处理时区你会发现热力图经常“晚一天”更新。第二是写回权限。要让 Actions 能自动提交workflow 必须声明permissions: contents: write否则git push会失败。如果你用私有仓库还要额外确认 Actions 的权限设置允许写入。4.4 展示方式怎么选自动化更新跑通之后再考虑展示位置。最省事的方式是把生成的图片放进仓库然后在 README 顶部引用![reading heatmap](./heatmap.png)这样别人打开你的项目主页第一眼就能看到阅读热力图。图片路径推荐用相对路径不要用/path这种绝对路径否则在不同分支和 Fork 场景下容易失效。如果你想做自己的个人主页也可以把生成结果发布到 GitHub Pages。这样你可以加更多交互鼠标悬停显示当天读了什么、点开某个月看详细记录。但这些都是可选项不应该是第一版的目标。5. 关键参数和判断标准5.1 颜色层级的判断标准同一个数据集不同阈值画出来的热力图完全不同。阈值设高了大部分格子都是浅色阈值设低了颜色全挤在最深一档。我用分位数的原因是它相对稳定。但分位数也有一个边界情况如果你的阅读数据里 0 值太多中位数可能等于 0。这种情况下应该先把值为 0 的格子单独归为“空档”只在非 0 数据上计算分位数否则颜色区分度依然拉不开。5.2 日期范围自然年还是滚动 365 天这个参数决定网格的跨度。自然年从 1 月 1 日到 12 月 31 日适合看年度目标。滚动 365 天始终展示最近一年适合持续追踪习惯不切换年份。GitHub 默认展示滚动一年但它有一个“按年”的跳转功能。个人项目我建议第一版做自然年因为数据量少、逻辑简单连“跨年时是否重置颜色”这种坑都不存在。等稳定运行一年后再考虑改成滚动 365 天。5.3 空值和缺失值的处理流程上空值和缺失值往往被混在一起但语义完全不同。缺失某一天没有任何记录表示“这条数据没有进入系统”。零值某一天有记录但统计值为 0表示“今天确实没有有效阅读”。在热力图上两者都显示为空白格子。但在统计连续天数和平均阅读时长时必须分开处理。我的做法是只有“有记录且值大于等于 0”的日期才参与统计缺失日期直接跳过。5.4 更新频率和失败率怎么判断定时任务不是配好就完事。你至少要监控两个指标单次更新是否成功。一周内更新成功率是不是稳定在 95% 以上。如果 GitHub Actions 每天跑一次偶尔一次失败不用紧张下一次成功后会补上。但如果连续三次失败就要去看工作流日志。常见原因是脚本依赖了某个外部数据源数据源接口变了或者 Python 包版本不兼容导致脚本崩溃。另外要注意 Actions 有月度额度限制。阅读热力图每天更新一次加生成图片和提交消耗很小完全够用。但不要为了“看起来实时”设置成每十分钟跑一次额度会很快被吃掉而且没有实际意义。6. 常见问题和排查顺序6.1 热力图全是浅色先查阈值现象跑完脚本图出来了但大部分格子都一个颜色。不要先怀疑绘图代码。先打印出值分布看看非零值是不是集中在很小的范围。如果你每天阅读时长在 20 到 40 分钟之间那么“30 分钟”这个中位数和“90 分钟”的极高值之间差距很大简单套固定阈值就会让颜色全部集中在低档。排查顺序先打印min、max、median再检查分级函数传入的阈值最后再看渲染结果。大多数情况下是阈值没按数据分布调整。6.2 日期整体偏移先查时区现象图里某一天的格子内容不对比如 1 月 1 日的数据显示在 1 月 2 日。这是典型的时区问题。数据源、聚合脚本、定时任务如果各用一套时区日期就会错位。排查顺序先确认数据源记录时间时用的时区再看脚本聚合时用的时区最后看 GitHub Actions 里 cron 的时区。建议统一用本地日期字符串彻底放弃时长戳。6.3 Actions 没跑起来先看日志和权限现象定时任务没有更新图片或者仓库里没有发现新提交。先打开仓库的 Actions 页面看最近一次运行是成功还是失败。成功但没有 new commit可能是因为脚本生成的内容没变化git diff判断为空所以跳过提交这是预期行为。如果失败看具体报错。最常见的三类权限不足workflow 缺少contents: write。脚本依赖没有安装workflow 里没有执行pip install。数据源访问失败外部接口返回非 200或 Cookie 过期。排查顺序永远是先看日志再改配置不要上来就重跑。6.4 图片不更新先看缓存和路径现象Actions 明明成功了但 README 里的图片还是旧版本。有两个可能。一个是浏览器缓存图片 URL 没变浏览器会继续使用本地缓存。另一个是仓库里图片路径被脚本覆盖到了错误分支或生成文件名和 README 引用不一致。排查顺序先在仓库里直接打开图片文件确认文件本身是否更新如果文件更新了再考虑加缓存刷新参数。这类问题和技术复杂度无关纯粹是路径或缓存细节认真看一遍即可。最后说几句这类项目真正值钱的地方不是热力图本身而是它逼着你建立一个稳定的个人数据流每天产生阅读记录、定期聚合、定时生成、长期沉淀。工具代码其实很少核心逻辑几百行就能写完难点全在“数据能不能稳定进来”和“口径能不能保持一致”。我更建议你把第一次测试拆成三步先用假数据跑通脚本再接入一周真实数据最后才配置 Actions 定时更新。每一步都验证通过后再进入下一步不要妄想一次搭完。如果你只是记录自己读了多少书手动维护一个 JSON 文件完全够用。如果你发现连续阅读天数、月对比、年份切换这些需求变得强烈再往 GitHub Pages 页面升级也不迟。这个项目最需要的不是复杂架构而是“今天也记录一下”的执行力。
返回列表