Skip to content

Instantly share code, notes, and snippets.

@jkrumbiegel
Created June 24, 2026 10:44
Show Gist options
  • Select an option

  • Save jkrumbiegel/b6b7628a51b025dbfa74dbff9b293a7b to your computer and use it in GitHub Desktop.

Select an option

Save jkrumbiegel/b6b7628a51b025dbfa74dbff9b293a7b to your computer and use it in GitHub Desktop.
Count new Julia package registrations per month from the General registry and plot the trend (RegistryActivity + AlgebraOfGraphics/CairoMakie)
using Pkg
Pkg.activate(; temp = true)
Pkg.add(["DataFrames", "CairoMakie", "AlgebraOfGraphics"])
Pkg.add(url = "https://github.com/JuliaEcosystem/RegistryActivity.jl")
using RegistryActivity, DataFrames, Dates, CairoMakie, AlgebraOfGraphics
const AoG = AlgebraOfGraphics
# RegistryActivity.clone_registry() clones via LibGit2, which fails on macOS with a
# SecureTransport error; clone with the system git and pass the path instead.
function local_general()
dir = joinpath(@__DIR__, "General")
isdir(dir) || run(`git clone https://github.com/JuliaRegistries/General.git $dir`)
dir
end
months, packages, _versions = RegistryActivity.registry_activity(local_general())
reg = DataFrame(month = collect(months), packages = collect(Int, packages))
reg.year = year.(reg.month)
# registry_activity reports the cumulative distinct-package count per monthly snapshot, so
# new registrations are the month-to-month increase. The Sep->Oct 2019 General cleanup
# removed packages, producing a one-off negative diff that is a removal, not a registration.
reg.new = [missing; diff(reg.packages)]
reg.new = [(!ismissing(x) && x < 0) ? missing : x for x in reg.new]
monthly = select(dropmissing(reg, :new), :month => :date, :new => :n)
yearly = combine(groupby(reg, :year), :new => (v -> sum(skipmissing(v))) => :n)
sort!(yearly, :year)
current_year = year(maximum(reg.month))
months_elapsed = count(==(current_year), reg.year)
yearly.annualized = [r.year == current_year ? round(Int, r.n * 12 / months_elapsed) : r.n
for r in eachrow(yearly)]
yearly.partial = yearly.year .== current_year
era_start = Date(2025, 2)
era = DataFrame(lo = [era_start], hi = [maximum(monthly.date)],
x = [era_start - Month(1)], y = [maximum(monthly.n) + 5.0])
fig = Figure(size = (1000, 760))
era_layers = AoG.data(era) * (
AoG.mapping(:lo, :hi) * AoG.visual(VSpan, color = (:orange, 0.1)) +
AoG.mapping(:lo) * AoG.visual(VLines, color = (:orange, 0.6), linestyle = :dash) +
AoG.mapping(:x, :y) * AoG.visual(Makie.Text, text = "Claude Code / coding-agent era",
color = :darkorange, align = (:right, :top), fontsize = 12))
# degree = 1: AoG's default degree-2 loess collapses on Date-typed x, because dates convert
# to a ~1e11 numeric scale that ill-conditions the quadratic term; a local-linear fit is robust.
monthly_layers = AoG.data(monthly) * AoG.mapping(:date => "", :n) * (
AoG.visual(Scatter, color = (:steelblue, 0.4), markersize = 7) +
AoG.smooth(span = 0.4, degree = 1) * AoG.visual(color = :firebrick))
AoG.draw!(fig[1, 1], era_layers + monthly_layers;
axis = (; title = "General registry: new packages registered per month",
ylabel = "new packages / month"))
bars = AoG.data(filter(:partial => identity, yearly)) * AoG.mapping(:year, :annualized) *
AoG.visual(BarPlot, color = (:firebrick, 0.2)) +
AoG.data(yearly) * AoG.mapping(:year, :n, color = :partial) * AoG.visual(BarPlot)
grid = AoG.draw!(fig[2, 1], bars,
AoG.scales(Color = (; palette = [:steelblue, :firebrick], legend = false));
axis = (; title = "New packages per year ($(current_year) annualized; 2018 & 2019 partial/affected by removal)",
xlabel = "year", ylabel = "packages / year",
xticks = minimum(yearly.year):maximum(yearly.year)))
for r in eachrow(yearly)
label = r.partial ? "$(r.n)\n(→$(r.annualized))" : string(r.n)
text!(grid[1].axis, r.year, r.partial ? r.annualized : r.n;
text = label, align = (:center, :bottom), fontsize = 10)
end
save("julia_new_packages.png", fig)
fig
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment