DEV Community

Cover image for Turn Your GitHub Contributions Into a City With Java and Quarkus
Markus
Markus

Posted on

Turn Your GitHub Contributions Into a City With Java and Quarkus

Turn your contribution calendar into an SVG skyline, add it to your profile, and add a DEV landmark with a small Java customization.

GitHub's contribution calendar gives you a familiar picture of your activity over the year. Each day has a square, and its color changes with the contribution level. Those same days can also be arranged into buildings, with a week becoming a small part of a city and busier weeks adding height to the skyline.

I built Quarkus Contribution City around this idea, using Java 21 and Quarkus to generate an SVG from GitHub's contribution data. The visual inspiration comes from Pink Pixel's skyline, and you can see the Java version with my real data on my GitHub profile. A scheduled GitHub Action keeps the image current.

For a Java developer, this is a small project you can follow from the API request through to the image. You can use the released action on your own profile, then change the drawing code locally once you have a feel for how the city is put together. The DEV example below adds a small piece of character art above the buildings and uses the built-in monochrome theme.

Add the City to Your Profile

Your GitHub profile README lives in a public repository with the same name as your account. For my account, that repository is myfear/myfear. Open your own profile repository, or create it with a README if you haven't used this GitHub feature before.

Copy the project's complete publishing workflow into .github/workflows/contribution-city.yml on the default branch. This example checks out your profile repository and the action's v1 release line. It then generates the image and publishes it to an output branch. The workflow includes the repository write permission needed for that last step.

The generation step uses the repository owner's username, so the same configuration can be copied between personal profile repositories. You can add the layout options to that step:

- name: Generate contribution city
  id: city
  uses: ./.city-action
  with:
    github-token: ${{ secrets.CONTRIBUTION_TOKEN || secrets.GITHUB_TOKEN }}
    username: ${{ github.repository_owner }}
    weeks: '53'
    height: '14'
    theme: github-dark
Enter fullscreen mode Exit fullscreen mode

The local path refers to the action checkout made earlier in the example workflow. The built-in GITHUB_TOKEN is the starting point for reading your visible contribution data. An optional CONTRIBUTION_TOKEN secret can supply a personal token when additional access is needed. The calendar returned by GitHub depends on the token's permissions and your account's contribution visibility settings.

After committing the workflow, open Actions in your profile repository, select publish-contribution-city, and choose Run workflow. A successful run creates contribution-city.svg on the output branch. Add this image reference to your profile README, replacing both occurrences of USERNAME:

![My GitHub contribution city](https://raw.githubusercontent.com/USERNAME/USERNAME/output/contribution-city.svg)
Enter fullscreen mode Exit fullscreen mode

The example also has a daily schedule for 03:17 UTC. Subsequent runs create a commit when the generated image changes. For this setup, the workflow and its run history are found in your profile repository, while the Java implementation is maintained in the action's repository.

Give Each Week a Building

The city is arranged in time order, with the most recent week on the right. Every building is two character cells wide, so all weeks occupy the same horizontal space. Weekly contribution totals determine the height, and daily contribution levels provide the window colors. Month labels beneath the street help you find your place in the year.

Height needs a little care. A very busy week can have many times the contributions of a quiet one. With a linear scale, those quieter weeks would be compressed close to the street. The city uses a logarithmic scale, which gives them more room while preserving the order of weekly totals.

For each week, the calculation can be expressed as:

int height = weeklyTotal == 0 ? 1
        : 1 + (int) Math.round(
                Math.log1p(weeklyTotal)
                        / Math.log1p(maximumWeeklyTotal)
                        * (maxHeight - 1));
Enter fullscreen mode Exit fullscreen mode

With a maximum of 20 contributions and a height limit of 14, a week with three contributions gets seven window rows. The busiest week gets 14, and a week with zero contributions keeps one row. Quiet periods therefore retain their position in the calendar. An antenna marks the busiest displayed week; when totals are tied, the most recent week receives it.

The two columns of windows are filled by cycling through the days of the week. This repeats the daily pattern in taller buildings, so window count is a visual texture. The weekly total controls height, while the window colors carry the daily contribution levels. Together, they give you a view of the pattern across your calendar.

You can change weeks to 12 for a shorter view. The tallest building is then calculated from those recent weeks, which lets the selected period use the available height. Keep this in mind when comparing images: the same week can appear taller in a shorter view. The displayed contribution total still covers the full calendar returned by GitHub.

Follow the Data Through Java

The released project uses Quarkus 3.33.1 with the Quarkus GitHub Action extension. That extension connects an @Action method to workflow inputs, outputs, and the job summary. Inside the method, the drawing process comes down to three steps:

ContributionCalendar calendar = contributionClient.fetch(
        configuration.username(),
        inputs.getGitHubToken().orElseThrow(),
        context.getGithubGraphQLUrl());
Scene scene = cityBuilder.build(calendar, configuration.options());
String svg = svgRenderer.render(scene, configuration.options().theme());
Enter fullscreen mode Exit fullscreen mode

The client reads the contribution calendar with one GraphQL request and converts the response into Java records for the calendar, weeks, and days. CityBuilder uses these records to place roofs, windows, and the street in a grid. SvgRenderer turns that grid into the final SVG. Changes to the skyline can therefore be made in plain Java classes, with the GitHub connection kept in the client.

The SVG contains the shapes and text needed to display the city. Adjacent characters with the same color are grouped during rendering, and their horizontal positions are set explicitly to keep the columns aligned. Identical data and options produce identical file contents, which allows the publisher to compare images before creating a commit.

The action sets up Java and JBang, builds the selected source revision, and runs the packaged application. Each invocation includes a Maven build, so allow for that when looking at the duration of a workflow run.

Change the Layout Locally

To try a change, install JDK 21 and JBang, with Git and Bash available. The repository includes a Maven wrapper. JBang is also used by the integration tests to run the packaged action through the same launcher used in GitHub Actions.

git clone --branch v1.0.0 https://github.com/myfear/quarkus-contribution-city.git
cd quarkus-contribution-city
./mvnw verify
Enter fullscreen mode Exit fullscreen mode

The tests use local test data and a local GraphQL server, so you can run them without GitHub credentials. They also generate SVG previews under target/previews. Open github-dark.svg, recent-12.svg, and empty.svg in a browser to compare a full calendar, a shorter period, and a calendar with zero contributions.

Start with the height calculation in CityBuilder, then run the same command again and compare the previews. The saved data stays the same between runs, which makes the effect of your change easier to see. The project is available under the Apache License 2.0, and the v1.0.0 release gives you a fixed starting point.

Give the City a DEV Landmark

Version 1.0.0 exposes weeks, height, and the two built-in themes through workflow inputs. For a custom shape, you extend the Java code in your fork. The Scene record gives you a grid of cells to change before the SVG is rendered, and that is where we'll add the DEV lettering.

Each cell has a character, a role such as roof or window, and the color information used by the renderer. A character mask is enough to describe a new shape: # marks a filled block and spaces leave the background visible. By arranging those blocks, you can draw letters, an arch, or a stepped tower.

For this example, the lettering is placed above the weekly buildings. It has its own fixed shape, while the buildings below continue to show the contribution calendar. The image uses a snapshot of my real account data, with 534 contributions returned by GitHub on October 6, 2026.

Monochrome DEV block lettering above a contribution city for myfear, with 534 contributions and monthly labels.

Decorative DEV lettering and the monochrome theme added to my contribution city. The weekly building heights and daily window patterns come from the same calendar used by the regular view.

Add this class to the existing city package in your fork, at src/main/java/com/themainthread/contributioncity/city/DevLandmark.java:

package com.themainthread.contributioncity.city;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.themainthread.contributioncity.render.Theme;
import com.themainthread.contributioncity.render.Themes;

public final class DevLandmark {
    public static final Theme THEME = Themes.MONO;

    private static final List<String> MASK = List.of(
            "####   #####  #   #",
            "#   #  #      #   #",
            "#   #  #      #   #",
            "#   #  ####   #   #",
            "#   #  #       # # ",
            "#   #  #       # # ",
            "####   #####    #  ");

    private DevLandmark() {
    }

    public static Scene addTo(Scene city) {
        int landmarkWidth = MASK.getFirst().length() * 2;
        int width = Math.max(city.width(), landmarkWidth);
        int left = (width - landmarkWidth) / 2;
        List<List<Cell>> rows = new ArrayList<>();
        Cell block = new Cell('█', CellKind.ROOF, 0, -1);
        for (String line : MASK) {
            List<Cell> row = new ArrayList<>(Collections.nCopies(width, Cell.SKY));
            for (int column = 0; column < line.length(); column++) {
                if (line.charAt(column) == '#') {
                    row.set(left + column * 2, block);
                    row.set(left + column * 2 + 1, block);
                }
            }
            rows.add(row);
        }
        rows.add(Collections.nCopies(width, Cell.SKY));
        for (List<Cell> sourceRow : city.rows()) {
            List<Cell> row = new ArrayList<>(sourceRow);
            row.addAll(Collections.nCopies(width - city.width(), Cell.SKY));
            rows.add(row);
        }
        return new Scene(city.username(), city.totalContributions(), rows);
    }
}
Enter fullscreen mode Exit fullscreen mode

The example reuses Themes.MONO, which defines the background, building shades, and five window colors. Those window colors follow GitHub's contribution levels from zero through four. The roof color is also used for the lettering, giving the decoration a light gray color across all seven rows. You can change the mask and keep the same theme while trying different shapes.

Inside addTo, each marked position is expanded to two cells because the renderer's cells are taller than they are wide. The lettering is centered, a blank row is added beneath it, and the original city rows are copied below. Short calendars receive extra background space on the right to make room for the lettering. The dates, counts, and order of the weekly buildings are preserved.

To use the decoration in your fork, import DevLandmark from the city package in ContributionCityAction. Replace the existing scene-building and rendering lines with:

Scene scene = cityBuilder.build(calendar, configuration.options());
Scene decorated = DevLandmark.addTo(scene);
String svg = svgRenderer.render(decorated, DevLandmark.THEME);
Enter fullscreen mode Exit fullscreen mode

This selects the monochrome palette in Java. The published @v1 action continues to accept github-dark and mono; your fork contains the additional drawing code. For a local preview, add the following after the existing Files.writeString call in PreviewTest.write, with the same import for DevLandmark:

Scene decorated = DevLandmark.addTo(builder.build(calendar, options));
Files.writeString(directory.resolve(name.replace(".svg", "-dev.svg")),
        renderer.render(decorated, DevLandmark.THEME));
Enter fullscreen mode Exit fullscreen mode

Run ./mvnw verify again and open target/previews/github-dark-dev.svg. The build also writes the shorter and empty versions with the -dev.svg suffix, so you can check how the decoration fits several calendars. When you're ready to use your fork on GitHub, update the second checkout in the publishing workflow to your fork's repository and a commit containing your changes. The generation step then runs the customized action from that checkout.

Try It With Your Own Contributions

Try Quarkus Contribution City with your own GitHub account and add the generated SVG to your profile. Start with the released action, then fork the project and change the character mask to give your city a landmark of its own. You could draw a stepped tower from your hometown or add your team's initials above the street, using the DEV version as a starting point.

Share a screenshot of your city in the comments. If you build a new shape, open a GitHub issue or a pull request with the SVG and your Java change. Add a test covering short and empty calendars so other readers can try your design with their own data.

Top comments (0)