Geographical distribution of typst community 🗺️ — by counting git commits

Previous discussion

Last month (2025-06), people discussed the geographical distribution of the typst community on Discord, and Laurenz Mädje replied:

In the last 7 days, our docs pages had visitors from ~120 countries. The top countries are Germany (14%), US (13%), Japan (9%), China (6%), France (6%), and Russia (4%).

mermaid.js code
pie title Docs pages visitors 
    "Germany": 14
    "US": 13
    "Japan": 9
    "China": 6
    "France": 6
    "Russia": 4
    "Other": 48

These numbers are based on IP addresses. They might not be accurate due to node jumps.

Git logs tell the timezone

git log shows the history of commits in a repo. Each commit includes author, message, and the date with timezone. In the following example, +0200 means Laurenz Stampfl was offset from UTC by two hours when committing the code.

$ git log
From 9fa41da5e8e194d17b3762d4a3b629dd49660105 Mon Sep 17 00:00:00 2001
From: Laurenz Stampfl
Date: Thu, 17 Jul 2025 20:42:35 +0200 👈🕗 Here's the timezone!
Subject: [PATCH] First working version for PDF export!

---
 .../typst-library/src/visualize/image/pdf.rs  | 87 ++++++++++++++++++-
 ...
 12 files changed, 196 insertions(+), 20 deletions(-)

With the help of git quick-stats --commits-by-timezone, we can calculate the distribution throughout the entire repo, kind of unravelling the geographical distribution of the community.

:warning: Note that such data may also be inaccurate. For example, timezones in GitHub Codespaces or Gitpod are usually UTC+0, and independent of the author’s real local time.

Results

(For aesthetic reasons, UTC+13 will be drawn in the same way as UTC-11.)

GitHub - typst/typst: A new markup-based typesetting system that is powerful and easy to learn.

GitHub - typst/packages: Packages for Typst.

GitHub - qjcg/awesome-typst: Awesome Typst Links

GitHub - Myriad-Dreamin/tinymist: Tinymist [ˈtaɪni mɪst] is an integrated language service for Typst [taɪpst].

GitHub - cetz-package/cetz: CeTZ: ein Typst Zeichenpaket - A library for drawing stuff with Typst.

GitHub - touying-typ/touying: Touying is a powerful package for creating presentation slides in Typst.

GitHub - Leedehai/typst-physics: physica: vectors, fields, differentials, derivatives, Dirac brakets, tensors, and more. See examples in the manual PDF.

More packages

GitHub - typst/hayagriva: Rusty bibliography management.

hayagriva

GitHub - Dherse/codly: A Typst package for even better code blocks

codly

GitHub - Jollywatt/typst-fletcher: Typst package for drawing diagrams with arrows, built on top of CeTZ.

fletcher

Even more data can be found in the source code:

main.typ
#let repo = sys.inputs.at("repo", default: "typst/typst")

/// Numbers of commits by timezone
///
/// Generated by https://git-quick-stats.sh `--commits-by-timezone`.
///
// `data.at(repo as str) = (timezones in hours as float, counts as int)`
#let data = (
  (
    "typst/typst": `Commits TimeZone
     30 -0800
     45 -0700
     10 -0600
     86 -0500
    157 -0400
     86 -0300
     71 +0000
   1399 +0100
   1602 +0200
     87 +0300
      5 +0500
      3 +0530
      2 +0600
      6 +0700
    134 +0800
     19 +0900
      9 +1000
      1 +1100
      1 +1200
      7 +1300`,
    "typst/packages": `Commits TimeZone
     38 -0800
     74 -0700
     34 -0600
     66 -0500
     74 -0400
     64 -0300
     76 +0000
    559 +0100
    700 +0200
     56 +0300
      1 +0400
     10 +0530
      1 +0545
      7 +0700
    447 +0800
     53 +0900
      5 +1000
      3 +1100
     12 +1200
     10 +1300`,
    "typst/hayagriva": `Commits TimeZone
      1 -0800
      1 -0700
      2 -0600
      2 -0500
      6 -0400
     25 -0300
      5 +0000
    152 +0100
    119 +0200
      1 +0300
      7 +0800`,
    "qjcg/awesome-typst": `Commits TimeZone
      1 -0800
     10 -0700
      9 -0600
     44 -0500
     81 -0400
      6 -0300
      9 +0000
     28 +0100
     53 +0200
      8 +0300
      3 +0500
      7 +0530
     42 +0800
      5 +0900
      1 +1000`,
    "Myriad-Dreamin/tinymist": `Commits TimeZone
      7 -0500
      5 -0400
      2 +0000
     15 +0100
     13 +0200
      8 +0300
   1404 +0800
      3 +0900`,
    "cetz-package/cetz": `Commits TimeZone
      1 -0800
      2 -0600
      4 -0500
      2 -0400
     24 +0000
    276 +0100
    227 +0200
      3 +0300
     18 +0800
      1 +0900
      1 +1200
      1 +1300`,
    "touying-typ/touying": `Commits TimeZone
      1 -0700
      3 -0400
      6 +0100
      3 +0200
    302 +0800
      7 +0900
      1 +1300`,
    "Leedehai/typst-physics": `Commits TimeZone
      1 -0700
     26 -0500
     31 -0400
      4 +0100
      5 +0200
     11 +0800
      2 +0900`,
    "Dherse/codly": `Commits TimeZone
      2 -0300
      1 +0000
    101 +0100
     37 +0200
      1 +0800
      1 +1000
      3 +1300`,
    "Jollywatt/typst-fletcher": `Commits TimeZone
      2 -0400
     63 +0000
     59 +0100
      5 +0200
      5 +0300
      3 +0800
    153 +1200
    316 +1300`,
    "polylux-typ/polylux": `Commits TimeZone
      9 -0400
      3 -0300
     81 +0100
    215 +0200
      1 +0300
      2 +0800`,
    "Typsium/typsium": `Commits TimeZone
     26 +0100
     18 +0200
     23 +0800`,
    "sahasatvik/typst-theorems": `Commits TimeZone
      2 -0400
      6 -0300
      1 +0100
     40 +0530`,
    "Marmare314/lemmify": `Commits TimeZone
      1 -0600
      2 -0300
      8 +0100
     48 +0200
      1 +0300`,
    "OrangeX4/typst-theorion": `Commits TimeZone
      3 +0100
      1 +0200
      1 +0700
     48 +0800
      1 +1300`,
    "Andrew15-5/rubby": `Commits TimeZone
     34 +0300`,
    "lilaq-project/lilaq": `Commits TimeZone
      1 -0400
      1 +0000
    157 +0100
    112 +0200
      1 +0800`,
    "typstyle-rs/typstyle": `Commits TimeZone
      4 -0700
      4 +0000
      2 +0200
    464 +0800
      3 +0900`,
  )
    .pairs()
    .map(((k, v)) => {
      let (header, ..records) = csv(
        bytes(v.text.split("\n").map(str.trim).join("\n")),
        delimiter: " ",
      )
      assert.eq(header, ("Commits", "TimeZone"))

      let (counts, timezones) = array.zip(..records)
      (
        k,
        (
          timezones.map(t => {
            assert.eq(t.len(), 5)
            let sgn = if t.first() == "+" { +1 } else { -1 }
            let hour = int(t.slice(1, 3))
            let minute = int(t.slice(3))
            sgn * (hour + minute / 60)
          }),
          counts.map(int),
        ),
      )
    })
    .to-dict()
)

#if false and "show-total-commits-of-each-repo" {
  set page(height: auto, width: auto, margin: 1em)

  table(
    columns: 2,
    align: (end, start),
    stroke: none,
    table.header()[*Total commits*][*Repo*],
    table.hline(),
    ..data
      .pairs()
      .map(((repo, data)) => {
        let (_, counts) = data
        (counts.sum(), repo)
      })
      .sorted(key: ((counts, repo)) => -counts)
      .map(((counts, repo)) => ([#counts], repo))
      .flatten(),
  )
  pagebreak()
}

#let (zones, counts) = data.at(repo)

#let width = 10cm
#set page(height: auto, width: width + 2 * 2em, margin: 2em)

#import "@preview/lilaq:0.4.0" as lq

#if repo != "typst/typst" {
  set align(center)
  set text(1.5em, weight: "bold")
  set par(spacing: 0em)
  repo
}

#image("World_Time_Zones_Map.svg")
// https://commons.wikimedia.org/wiki/File:World_Time_Zones_Map.svg
#v(-1em)
#{
  let leftmost = -11.2
  show: lq.set-diagram(
    width: width,
    xaxis: (
      label: if repo == "typst/typst" { [Timezone] },
      lim: (leftmost, leftmost + 24),
      ticks: range(-12, 13, step: 4),
      stroke: none,
    ),
    yaxis: (
      position: -1,
      format-ticks: (ticks, ..args) => {
        let result = lq.format-ticks-symlog(ticks, ..args)
        ticks
          .zip(result)
          .map(((tick, label)) => if tick < 1 {
            none
          } else {
            set text(0.8em)
            label
          })
      },
    ),
    yscale: "symlog",
  )
  show: lq.cond-set(lq.grid.with(kind: "x"), stroke: width / 24 + luma(90%))

  lq.diagram(
    lq.stem(
      // Normalize to [leftmost, leftmost + 24).
      zones.map(t => calc.rem-euclid(t - leftmost, 24) + leftmost),
      counts,
      mark-size: 8pt,
    ),
    ylim: (0.5, calc.max(..counts) * 1.3), // `auto`-max somehow does not work here
  )
}

#if repo == "typst/typst" {
  place(center, dx: 8em, dy: -12.3em, {
    set par(leading: 0.3em)
    let label = align(end)[Number of commits \ in #strong(repo)]
    place({
      set text(stroke: white + 3pt)
      label
    })
    label
  })
}
11 Likes

Oh, the previous version read +0530 as 5.3 hours, instead of 5.5 hours. Now corrected. :sweat_drops:

It’s interesting how you generated the gray background pattern to highlight every fourth hour. I was curious and had expected a bar plot or something similar but you actually used the grid. Good idea!

2 Likes