用Java和Quarkus把GitHub贡献日历变成城市天际线
GitHub 的贡献日历提供了一种已经极具标志性的方式,让你概览自己一年中的活动。每一天对应一个方块,其颜色会随贡献等级变化。当然,也可以把这些日期和贡献排列成建筑:一周变成城市的一小部分,而更繁忙的周则让天际线更高。
我抽出一些空闲时间,围绕这个想法构建了 Quarkus Contribution City,使用 Java 和 Quarkus 从 GitHub 的贡献数据生成 SVG。视觉灵感来自 Pink Pixel 的 skyline,你可以在我的 GitHub 个人资料上看到使用我真实数据的版本。一个定时的 GitHub Action 会在夜间更新图片。
对 Java 开发者来说,这是一个小项目,你可以从 API 请求一路跟踪到最终图像。你可以先在自己的个人资料中使用已发布的 action,然后在了解城市是如何组合起来之后,在本地更改绘制代码。本文中,我创建了一个特殊的 DZone 示例,在建筑上方添加了一小段字符画。
将城市添加到你的个人资料
你的 GitHub 个人资料 README 位于一个与你账户同名的公开仓库中。对于我的账户,该仓库是 myfear/myfear。打开你自己的个人资料仓库;如果你之前没有使用过这个 GitHub 功能,也可以创建它并添加一个 README。
将项目完整的发布工作流复制到默认分支上的 .github/workflows/contribution-city.yml。该示例会检出你的个人资料仓库以及该 action 的 v1 发布线。然后它会生成图片并将其发布到 output 分支。该工作流包含最后一步所需的仓库写入权限。
生成步骤使用仓库所有者的用户名,因此同一配置可以在个人资料仓库之间复制。你可以将布局选项添加到该步骤中:
该本地路径指的是示例工作流中前面执行的动作检出目录。内置的 GITHUB_TOKEN 是读取你的可见贡献数据的起点。当需要额外访问权限时,可选的 CONTRIBUTION_TOKEN secret 可提供个人令牌。GitHub 返回的日历取决于令牌权限以及你账户的贡献可见性设置。
提交工作流后,在你的个人资料仓库中打开 Actions,选择 publish-contribution-city,然后选择 Run workflow。成功运行会在 output 分支上创建 contribution-city.svg。将以下图片引用添加到你的个人资料 README,并替换两处 USERNAME:

该示例还设置了每天 03:17 UTC 的定时计划。后续运行时,如果生成的图片发生变化,就会创建一次提交。在这种设置中,工作流及其运行历史位于你的个人资料仓库中,而 Java 实现则维护在该 action 的仓库中。
为每一周建一栋楼
城市按时间顺序排列,最近的一周在右侧。每栋建筑宽两个字符单元,因此所有周都占据相同的水平空间。每周贡献总数决定高度,而每日贡献等级决定窗户颜色。街道下方的月份标签可帮助你在一年中定位。
高度需要多考虑一点。非常繁忙的一周,其贡献数可能是安静一周的很多倍。如果使用线性比例,那些较安静的周会被压缩到接近街道的位置。城市使用对数比例,这能给它们更多空间,同时保留每周总数的顺序。对于每一周,计算可以表示为:
在最大贡献数为 20、高度上限为 14 的情况下,贡献数为 3 的一周会有七行窗户。最繁忙的一周高度为 14,而零贡献的一周保持一行。因此,安静时期在日历中仍能保留自己的位置。
天线用于标记显示范围内最繁忙的一周;如果总数相同,则由最近的一周获得天线。两列窗户通过循环遍历一周七天来填充。这样会在更高的建筑中重复每日模式,因此窗户数量只是一种视觉纹理。每周总数控制高度,而窗户颜色承载每日贡献等级。二者结合,让你看到整个日历中的模式。
你可以将 weeks 改为 12,获得更短的视图。此时最高建筑会根据这些最近的周计算,从而让所选时段利用可用高度。比较图片时请记住:同一周在较短视图中可能显得更高。显示的贡献总数仍覆盖 GitHub 返回的完整日历。
用 Java 跟踪数据流程
已发布的项目使用 Quarkus 以及 Quarkus GitHub Action 扩展。该扩展将 @Action 方法连接到工作流输入、输出和作业摘要。在该方法内部,绘制过程可归结为三个步骤:
客户端通过一次 GraphQL 请求读取贡献日历,并将响应转换为用于日历、周和日的 Java record。CityBuilder 使用这些 record 在网格中放置屋顶、窗户和街道。SvgRenderer 将该网格转换为最终 SVG。因此,可以在普通 Java 类中更改天际线,而 GitHub 连接仍保留在客户端中。
SVG 包含显示城市所需的形状和文本。渲染时,颜色相同的相邻字符会被分组,并且会显式设置它们的水平位置以保持列对齐。相同的数据和选项会生成相同的文件内容,这使发布器可以在创建提交前比较图片。该 action 会配置 Java 和 JBang,构建选定的源代码修订版本,并运行打包后的应用。
在本地更改布局
要尝试更改,请安装 JDK 21 和 JBang,并确保 Git 和 Bash 可用。仓库包含一个 Maven Wrapper。集成测试也使用 JBang,通过 GitHub Actions 中使用的同一启动器运行打包后的 action。
测试使用本地测试数据和本地 GraphQL 服务器,因此你无需 GitHub 凭据即可运行它们。测试还会在 target/previews 下生成 SVG 预览。在浏览器中打开 github-dark.svg、recent-12.svg 和 empty.svg,比较完整日历、较短时段和零贡献日历。
先从 CityBuilder 中的高度计算开始修改,然后再次运行同一命令并比较预览。保存的数据在多次运行之间保持不变,因此更容易看出更改的效果。该项目采用 Apache License 2.0,v1.0.0 发布版为你提供了一个固定的起点。
为城市添加一个 DZone 地标
1.0.0 版通过工作流输入暴露 weeks、height 和两个内置主题。若要自定义形状,请在 fork 中扩展 Java 代码。Scene record 提供了一个可在渲染 SVG 之前修改的单元格网格,我们将在这里添加 DZone 字样。
每个单元格都有一个字符、一个角色(例如屋顶或窗户),以及渲染器使用的颜色信息。字符掩码足以描述一个新形状:# 表示填充块,空格则让背景保持可见。通过排列这些块,你可以绘制字母、拱门或阶梯塔。
在本示例中,字样放置在各周建筑的上方。它具有自己固定的形状,而下方的建筑继续显示贡献日历。该图片使用我真实账户数据的快照:2026 年 10 月 5 日我撰写本文时,GitHub 返回了 534 次贡献。
[LOADING...]
在我的贡献城市中加入受 DZone 启发的配色和装饰字样。各周建筑高度和每日窗户模式来自常规视图使用的同一日历。
将此类添加到 fork 中现有的 city 包,路径为 src/main/java/com/themainthread/contributioncity/city/DZoneLandmark.java:
该主题定义了背景、两种建筑阴影、屋顶、街道和标签颜色,随后是五种窗户颜色。这些窗户颜色对应 GitHub 从 0 到 4 的贡献等级。在这里,屋顶颜色也用于字样,因此装饰在全部七行中保持一致的橙色。
在 addTo 中,每个标记位置都会扩展为两个单元格,因为渲染器的单元格高度大于宽度。字样居中,其下添加一个空白行,然后将原始城市行复制到下方。较短的日历会在右侧获得额外的背景空间,以便为字样留出位置。各周建筑的日期、计数和顺序均保持不变。
要在你的 fork 中使用该装饰,请在 ContributionCityAction 中从 city 包导入 DZoneLandmark。将现有的场景构建和渲染代码行替换为:
这会以 Java 方式选择示例的配色。已发布的 @v1 action 继续接受 github-dark 和 mono;你的 fork 包含额外的绘制代码。
如需本地预览,请在 PreviewTest.write 中现有的 Files.writeString 调用之后添加以下内容,并使用相同的 DZoneLandmark 导入:
再次运行 ./mvnw verify 并打开 target/previews/github-dark-dzone.svg。构建还会写入带有 -dzone.svg 后缀的较短版本和空版本,因此你可以检查该装饰在多种日历中的适配情况。
当你准备在 GitHub 上使用 fork 时,请将发布工作流中的第二个检出步骤更新为你的 fork 仓库以及包含你更改的提交。随后,生成步骤将从该检出目录运行自定义 action。
用你自己的贡献试一试
用你自己的 GitHub 账户试用 Quarkus Contribution City,并将生成的 SVG 添加到个人资料中。先从已发布的 action 开始,然后 fork 项目并更改字符掩码,为你的城市添加一个专属地标。你可以以 DZone 版本为起点,绘制一座来自你家乡的阶梯塔,或在街道上方添加你团队的缩写。可在 GitHub issue 中分享你的版本;如果你想贡献新形状,也可以提交 pull request。