From b4cb18424900dd659dcc615bef4efc0ae9997c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 14 Oct 2026 00:45:20 +0200 Subject: [PATCH 1/6] feat(render): chart GitHub stars added per day with a 7-day rolling average Cumulative stars hide whether one week grew 2x or 1/3 of the previous one. Add charts/stars_growth.svg: daily net additions (last 365 days) with a 7-day rolling average, mirroring the diff + 7D rolling mean that ActivityWatch/stats uses for download growth. The per-day series combines stars.csv (by starred_at) with the daily stargazers_count snapshots once stars.csv stops, interpolating missing snapshot days so collection gaps don't show up as spikes. Also fix 'Stars gained, last 30 days': it counted stars.csv rows only, which have not updated since 2026-09-14 (no STATS_GH_TOKEN), so it showed 1 instead of 36. It now uses the same combined series, and the table gains last-7 / previous-7 day rows. --- README.md | 7 +- charts/stars_growth.svg | 4930 +++++++++++++++++++++++++++++++++++++++ data/summary.json | 4 +- render.py | 80 +- 4 files changed, 5015 insertions(+), 6 deletions(-) create mode 100644 charts/stars_growth.svg diff --git a/README.md b/README.md index 8d25a34..8b32a34 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Usage and community stats for [gptme](https://github.com/gptme/gptme), collected [![gptme GitHub stars over time](charts/stars.svg)](data/stars.csv) +[![GitHub stars added per day, 7-day rolling average](charts/stars_growth.svg)](data/daily.csv) + [![gptme PyPI downloads](charts/pypi.svg)](data/pypi_daily.csv) Embed the star chart elsewhere with: @@ -24,7 +26,9 @@ _Latest snapshot: 2026-10-13 (UTC). Machine-readable: [`data/summary.json`](data | Metric | Value | |---|---:| | GitHub stars | 4,448 | -| Stars gained, last 30 days | 1 | +| Stars gained, last 7 days | 3 | +| Stars gained, previous 7 days | 8 | +| Stars gained, last 30 days | 36 | | Forks | 451 | | Watchers | 40 | | Contributors | 57 | @@ -52,6 +56,7 @@ _Latest snapshot: 2026-10-13 (UTC). Machine-readable: [`data/summary.json`](data ### Definitions and caveats - **Stars** (`daily.csv`) is the repo's `stargazers_count` that day. People can unstar, so this number can go down. +- **Stars added per day** (`charts/stars_growth.svg`, and the "stars gained" rows above) is the day-over-day change of one daily series: `stars.csv` counts while they cover a day, `daily.csv` snapshots after that. Before snapshots it counts current stargazers by the day they starred; with snapshots it is net of unstars and can be negative. A snapshot is taken around 04:17 UTC, so a snapshot day's change mostly reflects the previous UTC day. Missing snapshot days are filled by linear interpolation, so a collection gap spreads out instead of showing as a spike. The chart covers the last 365 days; the October 2024 launch week (~1,400 stars) would flatten anything wider. - **`stars.csv`** holds only current stargazers. When someone unstars, their row disappears at the next resync, so the cumulative chart shows today's stargazers by the date they starred, not a historical peak. `daily.csv` records what the total actually was on each day. Logins are deliberately not stored: the charts only need timestamps, and there is no reason to publish a permanent, append-only list of who starred. - **Watchers** is `subscribers_count` (people watching the repo), not the legacy `watchers_count` alias for stars. - **Open issues** excludes pull requests. GitHub's `open_issues_count` includes PRs, so open PRs are counted separately and subtracted. diff --git a/charts/stars_growth.svg b/charts/stars_growth.svg new file mode 100644 index 0000000..5715c8f --- /dev/null +++ b/charts/stars_growth.svg @@ -0,0 +1,4930 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/summary.json b/data/summary.json index e7c6912..f9afe92 100644 --- a/data/summary.json +++ b/data/summary.json @@ -3,7 +3,9 @@ "source": "https://github.com/gptme/stats", "repo": "gptme/gptme", "stars": 4448, - "stars_last_30d": 1, + "stars_last_7d": 3, + "stars_prev_7d": 8, + "stars_last_30d": 36, "forks": 451, "watchers": 40, "open_issues": 7, diff --git a/render.py b/render.py index f26a469..303f29c 100644 --- a/render.py +++ b/render.py @@ -29,6 +29,7 @@ README = ROOT / "README.md" START, END = "", "" MIN_DAYS_FOR_DOWNLOADS_CHART = 14 +STAR_GROWTH_DAYS = 365 # window of the star growth chart; the 2024 launch spike would flatten anything wider # Light chart card; reads fine embedded on both GitHub light and dark pages. SURFACE = "#fcfcfb" @@ -143,6 +144,68 @@ def render_stars(stars: list[str], daily: list[dict[str, str]], as_of: str) -> N save(fig, "stars.svg") +def star_totals(stars: list[str], daily: list[dict[str, str]]) -> dict[dt.date, int]: + """Cumulative star count for every calendar day, from the first star to the last snapshot. + + Days covered by stars.csv count current stargazers by the day they starred. After + that, the series continues from the daily stargazers_count snapshots (net of unstars). + Missing snapshot days are filled by linear interpolation, so a gap in collection + spreads its stars over the gap instead of showing up as a one-day spike. + """ + per_day = Counter(dt.date.fromisoformat(s[:10]) for s in stars) + known: dict[dt.date, int] = {} + running = 0 + for day in sorted(per_day): + running += per_day[day] + known[day] = running + last_star_day = max(known) + for row in daily: + day = dt.date.fromisoformat(row["date"]) + if day > last_star_day and row.get("stars", "").isdigit(): + known[day] = int(row["stars"]) + days = sorted(known) + totals: dict[dt.date, int] = {} + for a, b in zip(days, days[1:] + [None]): + totals[a] = known[a] + if b is None: + break + gap = (b - a).days + for i in range(1, gap): + if b <= last_star_day: + # stars.csv has an exact count for every day; days without a row had no stars. + totals[a + dt.timedelta(days=i)] = known[a] + else: + totals[a + dt.timedelta(days=i)] = known[a] + round((known[b] - known[a]) * i / gap) + return totals + + +def render_star_growth(totals: dict[dt.date, int]) -> None: + days = sorted(totals) + added = {d: totals[d] - totals[d - dt.timedelta(days=1)] for d in days[1:]} + end = days[-1] + start = max(days[1], end - dt.timedelta(days=STAR_GROWTH_DAYS - 1)) + shown = [d for d in sorted(added) if d >= start] + xs, ys = rolling_mean(sorted(added), added, window=7) + avg = [(x, y) for x, y in zip(xs, ys) if x >= start] + week = sum(added[end - dt.timedelta(days=i)] for i in range(7)) + prev = sum(added[end - dt.timedelta(days=i)] for i in range(7, 14)) + fig, ax = new_figure( + "GitHub stars added per day (7-day rolling avg)", + f"Net new stargazers of gptme/gptme per day, last {STAR_GROWTH_DAYS} days. " + f"Last 7 days: {week:,}, previous 7: {prev:,}", + ) + ax.bar(shown, [added[d] for d in shown], width=0.8, color=BLUE_LIGHT, linewidth=0, label="Daily") + if avg: + ax.plot([x for x, _ in avg], [y for _, y in avg], color=BLUE, linewidth=2, label="7-day average") + end_label(ax, avg[-1][0], avg[-1][1], f"{avg[-1][1]:,.1f}/day") + ax.axhline(0, color=BASELINE, linewidth=1, zorder=1) + ax.set_xlim(start - dt.timedelta(days=1), end + dt.timedelta(days=1)) + lo = min(0, min(added[d] for d in shown)) + ax.set_ylim(lo * 1.08, max(1, max(added[d] for d in shown)) * 1.08) + ax.legend(loc="upper left", fontsize=9, labelcolor=INK_2, handlelength=1.5) + save(fig, "stars_growth.svg") + + def rolling_mean(dates: list[dt.date], values: dict[dt.date, int], window: int = 30): xs, ys = [], [] first = dates[0] @@ -196,10 +259,13 @@ def pypi_window(pypi: list[dict[str, str]], days: int) -> int: return sum(int(r["downloads"]) for r in pypi[-days:]) -def build_summary(daily, stars, pypi) -> dict: +def build_summary(daily, totals, pypi) -> dict: latest = daily[-1] as_of = dt.date.fromisoformat(latest["date"]) - cutoff = (as_of - dt.timedelta(days=30)).isoformat() + + def stars_gained(days: int, offset: int = 0) -> int | None: + hi, lo = as_of - dt.timedelta(days=offset), as_of - dt.timedelta(days=offset + days) + return totals[hi] - totals[lo] if hi in totals and lo in totals else None def num(key: str) -> int | None: return int(latest[key]) if latest.get(key) else None @@ -209,7 +275,9 @@ def num(key: str) -> int | None: "source": "https://github.com/gptme/stats", "repo": "gptme/gptme", "stars": num("stars"), - "stars_last_30d": sum(1 for s in stars if s[:10] > cutoff), + "stars_last_7d": stars_gained(7), + "stars_prev_7d": stars_gained(7, offset=7), + "stars_last_30d": stars_gained(30), "forks": num("forks"), "watchers": num("watchers"), "open_issues": num("open_issues"), @@ -241,6 +309,8 @@ def update_readme(s: dict, has_downloads_chart: bool) -> None: "| Metric | Value |", "|---|---:|", f"| GitHub stars | {fmt(s['stars'])} |", + f"| Stars gained, last 7 days | {fmt(s['stars_last_7d'])} |", + f"| Stars gained, previous 7 days | {fmt(s['stars_prev_7d'])} |", f"| Stars gained, last 30 days | {fmt(s['stars_last_30d'])} |", f"| Forks | {fmt(s['forks'])} |", f"| Watchers | {fmt(s['watchers'])} |", @@ -271,8 +341,10 @@ def main() -> None: if not (daily and stars and pypi): raise SystemExit("missing data, run collect.py first") - summary = build_summary(daily, stars, pypi) + totals = star_totals(stars, daily) + summary = build_summary(daily, totals, pypi) render_stars(stars, daily, summary["generated_at"]) + render_star_growth(totals) render_pypi(pypi) has_downloads = render_downloads(daily) (DATA / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") From c8db4e7e26bfa66c4c6b4f71099e842102660a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 14 Oct 2026 01:11:39 +0200 Subject: [PATCH 2/6] fix(render): base star growth on snapshots even when stars.csv is current Use daily.csv snapshots from the first snapshot on and stars.csv only before it (offset to meet the first snapshot), so the growth series stays net of unstars after the stargazer token is restored. Skip the growth chart when there is less than two days of history. --- README.md | 2 +- render.py | 56 ++++++++++++++++++++++++++++--------------------------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 8b32a34..e7ec38a 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ _Latest snapshot: 2026-10-13 (UTC). Machine-readable: [`data/summary.json`](data ### Definitions and caveats - **Stars** (`daily.csv`) is the repo's `stargazers_count` that day. People can unstar, so this number can go down. -- **Stars added per day** (`charts/stars_growth.svg`, and the "stars gained" rows above) is the day-over-day change of one daily series: `stars.csv` counts while they cover a day, `daily.csv` snapshots after that. Before snapshots it counts current stargazers by the day they starred; with snapshots it is net of unstars and can be negative. A snapshot is taken around 04:17 UTC, so a snapshot day's change mostly reflects the previous UTC day. Missing snapshot days are filled by linear interpolation, so a collection gap spreads out instead of showing as a spike. The chart covers the last 365 days; the October 2024 launch week (~1,400 stars) would flatten anything wider. +- **Stars added per day** (`charts/stars_growth.svg`, and the "stars gained" rows above) is the day-over-day change of one daily series: the `daily.csv` snapshots from the first snapshot (2026-09-14) on, `stars.csv` before that. Before snapshots it counts current stargazers by the day they starred; with snapshots it is net of unstars and can be negative. A snapshot is taken around 04:17 UTC, so a snapshot day's change mostly reflects the previous UTC day. Missing snapshot days are filled by linear interpolation, so a collection gap spreads out instead of showing as a spike. The chart covers the last 365 days; the October 2024 launch week (~1,400 stars) would flatten anything wider. - **`stars.csv`** holds only current stargazers. When someone unstars, their row disappears at the next resync, so the cumulative chart shows today's stargazers by the date they starred, not a historical peak. `daily.csv` records what the total actually was on each day. Logins are deliberately not stored: the charts only need timestamps, and there is no reason to publish a permanent, append-only list of who starred. - **Watchers** is `subscribers_count` (people watching the repo), not the legacy `watchers_count` alias for stars. - **Open issues** excludes pull requests. GitHub's `open_issues_count` includes PRs, so open PRs are counted separately and subtracted. diff --git a/render.py b/render.py index 303f29c..8688941 100644 --- a/render.py +++ b/render.py @@ -145,50 +145,51 @@ def render_stars(stars: list[str], daily: list[dict[str, str]], as_of: str) -> N def star_totals(stars: list[str], daily: list[dict[str, str]]) -> dict[dt.date, int]: - """Cumulative star count for every calendar day, from the first star to the last snapshot. + """Star count for every calendar day, from the first star to the last snapshot. - Days covered by stars.csv count current stargazers by the day they starred. After - that, the series continues from the daily stargazers_count snapshots (net of unstars). + From the first daily.csv snapshot on, this is the snapshot stargazers_count (net of + unstars), whether or not stars.csv is current. Before snapshots existed, it is + stars.csv (current stargazers by the day they starred), shifted by a constant so the + two meet at the first snapshot; only day-over-day differences are meaningful there. Missing snapshot days are filled by linear interpolation, so a gap in collection spreads its stars over the gap instead of showing up as a one-day spike. """ + snaps = { + dt.date.fromisoformat(r["date"]): int(r["stars"]) for r in daily if r.get("stars", "").isdigit() + } per_day = Counter(dt.date.fromisoformat(s[:10]) for s in stars) - known: dict[dt.date, int] = {} - running = 0 - for day in sorted(per_day): - running += per_day[day] - known[day] = running - last_star_day = max(known) - for row in daily: - day = dt.date.fromisoformat(row["date"]) - if day > last_star_day and row.get("stars", "").isdigit(): - known[day] = int(row["stars"]) - days = sorted(known) + first_snap = min(snaps) if snaps else max(per_day) + dt.timedelta(days=1) totals: dict[dt.date, int] = {} - for a, b in zip(days, days[1:] + [None]): - totals[a] = known[a] - if b is None: - break + if per_day: + day, running = min(per_day), 0 + while day <= first_snap: + running += per_day.get(day, 0) + totals[day] = running + day += dt.timedelta(days=1) + if first_snap in snaps: + offset = snaps[first_snap] - totals[first_snap] + totals = {d: v + offset for d, v in totals.items()} + days = sorted(snaps) + for a, b in zip(days, days[1:]): gap = (b - a).days for i in range(1, gap): - if b <= last_star_day: - # stars.csv has an exact count for every day; days without a row had no stars. - totals[a + dt.timedelta(days=i)] = known[a] - else: - totals[a + dt.timedelta(days=i)] = known[a] + round((known[b] - known[a]) * i / gap) - return totals + totals[a + dt.timedelta(days=i)] = snaps[a] + round((snaps[b] - snaps[a]) * i / gap) + totals.update(snaps) + return dict(sorted(totals.items())) -def render_star_growth(totals: dict[dt.date, int]) -> None: +def render_star_growth(totals: dict[dt.date, int]) -> bool: days = sorted(totals) + if len(days) < 2: + return False added = {d: totals[d] - totals[d - dt.timedelta(days=1)] for d in days[1:]} end = days[-1] start = max(days[1], end - dt.timedelta(days=STAR_GROWTH_DAYS - 1)) shown = [d for d in sorted(added) if d >= start] xs, ys = rolling_mean(sorted(added), added, window=7) avg = [(x, y) for x, y in zip(xs, ys) if x >= start] - week = sum(added[end - dt.timedelta(days=i)] for i in range(7)) - prev = sum(added[end - dt.timedelta(days=i)] for i in range(7, 14)) + week = sum(added.get(end - dt.timedelta(days=i), 0) for i in range(7)) + prev = sum(added.get(end - dt.timedelta(days=i), 0) for i in range(7, 14)) fig, ax = new_figure( "GitHub stars added per day (7-day rolling avg)", f"Net new stargazers of gptme/gptme per day, last {STAR_GROWTH_DAYS} days. " @@ -204,6 +205,7 @@ def render_star_growth(totals: dict[dt.date, int]) -> None: ax.set_ylim(lo * 1.08, max(1, max(added[d] for d in shown)) * 1.08) ax.legend(loc="upper left", fontsize=9, labelcolor=INK_2, handlelength=1.5) save(fig, "stars_growth.svg") + return True def rolling_mean(dates: list[dt.date], values: dict[dt.date, int], window: int = 30): From aabfa089dc042f00d7e8d6a8114c1cc7c4053519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 14 Oct 2026 01:15:18 +0200 Subject: [PATCH 3/6] fix(render): don't add a synthetic day when there are no star snapshots --- render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/render.py b/render.py index 8688941..8594898 100644 --- a/render.py +++ b/render.py @@ -158,7 +158,8 @@ def star_totals(stars: list[str], daily: list[dict[str, str]]) -> dict[dt.date, dt.date.fromisoformat(r["date"]): int(r["stars"]) for r in daily if r.get("stars", "").isdigit() } per_day = Counter(dt.date.fromisoformat(s[:10]) for s in stars) - first_snap = min(snaps) if snaps else max(per_day) + dt.timedelta(days=1) + # Without snapshots, stars.csv is all there is; windows it doesn't cover stay n/a. + first_snap = min(snaps) if snaps else max(per_day, default=dt.date.min) totals: dict[dt.date, int] = {} if per_day: day, running = min(per_day), 0 From 754cb753a2a7ef21aac989484f068eed9e162823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 14 Oct 2026 01:18:15 +0200 Subject: [PATCH 4/6] ci: temporary probe of star endpoints with GITHUB_TOKEN --- .github/workflows/probe-stars.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/probe-stars.yml diff --git a/.github/workflows/probe-stars.yml b/.github/workflows/probe-stars.yml new file mode 100644 index 0000000..5599935 --- /dev/null +++ b/.github/workflows/probe-stars.yml @@ -0,0 +1,20 @@ +# TEMPORARY: probe which star endpoints the workflow GITHUB_TOKEN can read. Removed before merge. +name: probe stars +on: pull_request +permissions: + contents: read +jobs: + probe: + runs-on: ubuntu-latest + steps: + - env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "== REST stargazers star+json" + curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.star+json" "https://api.github.com/repos/gptme/gptme/stargazers?per_page=1&page=4000" + echo "== REST stargazers plain" + curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/gptme/gptme/stargazers?per_page=1" + echo "== GraphQL stargazers" + curl -s -X POST -H "Authorization: Bearer $GH_TOKEN" https://api.github.com/graphql \ + -d '{"query":"{ repository(owner:\"gptme\", name:\"gptme\") { stargazers(first:2, orderBy:{field:STARRED_AT, direction:DESC}) { totalCount edges { starredAt } } } }"}' + echo From 44c1e554849a40a00e454d25d8abbcd39b954e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 14 Oct 2026 01:21:06 +0200 Subject: [PATCH 5/6] feat: full-history star growth chart on a log scale; collect with GITHUB_TOKEN only - stars_growth.svg now covers the full history on a symlog y-axis, so the October 2024 launch week (1,393 stars) and today's ~1/day both stay readable, and the busiest week is labelled. - Drop the optional STATS_GH_TOKEN secret. The workflow uses the built-in GITHUB_TOKEN only. GitHub refuses stargazer listings to it (403, REST and GraphQL alike) and to anonymous requests (401), so stars.csv only grows on local runs with a user token; every chart and summary number continues from the daily stargazers_count snapshots, so nothing goes stale. - Remove the temporary token probe workflow. --- .github/workflows/collect.yml | 6 +- .github/workflows/probe-stars.yml | 20 - README.md | 3 +- charts/stars_growth.svg | 13763 ++++++++++++++++++++++------ collect.py | 21 +- render.py | 56 +- 6 files changed, 11038 insertions(+), 2831 deletions(-) delete mode 100644 .github/workflows/probe-stars.yml diff --git a/.github/workflows/collect.yml b/.github/workflows/collect.yml index 9bb8eb2..43b71c9 100644 --- a/.github/workflows/collect.yml +++ b/.github/workflows/collect.yml @@ -28,10 +28,10 @@ jobs: - name: Collect env: + # The built-in token is all this needs. GitHub doesn't let it list stargazers + # (403), so stars.csv only grows on local runs with a user token; the charts + # use the daily stargazers_count snapshots instead, so they never go stale. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Optional user token (public read is enough). Without it, per-star timestamps - # stop updating and the star chart continues from daily stargazers_count. - STATS_GH_TOKEN: ${{ secrets.STATS_GH_TOKEN }} FULL_STARS: ${{ inputs.full_stars || false }} run: | if [ "$FULL_STARS" = "true" ]; then diff --git a/.github/workflows/probe-stars.yml b/.github/workflows/probe-stars.yml deleted file mode 100644 index 5599935..0000000 --- a/.github/workflows/probe-stars.yml +++ /dev/null @@ -1,20 +0,0 @@ -# TEMPORARY: probe which star endpoints the workflow GITHUB_TOKEN can read. Removed before merge. -name: probe stars -on: pull_request -permissions: - contents: read -jobs: - probe: - runs-on: ubuntu-latest - steps: - - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - echo "== REST stargazers star+json" - curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.star+json" "https://api.github.com/repos/gptme/gptme/stargazers?per_page=1&page=4000" - echo "== REST stargazers plain" - curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/gptme/gptme/stargazers?per_page=1" - echo "== GraphQL stargazers" - curl -s -X POST -H "Authorization: Bearer $GH_TOKEN" https://api.github.com/graphql \ - -d '{"query":"{ repository(owner:\"gptme\", name:\"gptme\") { stargazers(first:2, orderBy:{field:STARRED_AT, direction:DESC}) { totalCount edges { starredAt } } } }"}' - echo diff --git a/README.md b/README.md index e7ec38a..e124fa6 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,8 @@ _Latest snapshot: 2026-10-13 (UTC). Machine-readable: [`data/summary.json`](data ### Definitions and caveats - **Stars** (`daily.csv`) is the repo's `stargazers_count` that day. People can unstar, so this number can go down. -- **Stars added per day** (`charts/stars_growth.svg`, and the "stars gained" rows above) is the day-over-day change of one daily series: the `daily.csv` snapshots from the first snapshot (2026-09-14) on, `stars.csv` before that. Before snapshots it counts current stargazers by the day they starred; with snapshots it is net of unstars and can be negative. A snapshot is taken around 04:17 UTC, so a snapshot day's change mostly reflects the previous UTC day. Missing snapshot days are filled by linear interpolation, so a collection gap spreads out instead of showing as a spike. The chart covers the last 365 days; the October 2024 launch week (~1,400 stars) would flatten anything wider. +- **Stars added per day** (`charts/stars_growth.svg`, and the "stars gained" rows above) is the day-over-day change of one daily series: the `daily.csv` snapshots from the first snapshot (2026-09-14) on, `stars.csv` before that. Before snapshots it counts current stargazers by the day they starred; with snapshots it is net of unstars and can be negative. A snapshot is taken around 04:17 UTC, so a snapshot day's change mostly reflects the previous UTC day. Missing snapshot days are filled by linear interpolation, so a collection gap spreads out instead of showing as a spike. The chart covers the full history on a log scale, so a 1/day week stays readable next to the October 2024 launch week (~1,400 stars), and doubling looks the same at any level. +- **`stars.csv` needs a user token to update.** GitHub returns 401 for unauthenticated stargazer listings and 403 for the workflow's `GITHUB_TOKEN` (REST and GraphQL alike), so the daily job can't refresh it and keeps it as is. Nothing depends on it staying current: the cumulative chart continues from the `daily.csv` snapshots after its last row, and the growth chart and "stars gained" rows use snapshots for every day they exist. Running `collect.py` locally with `GITHUB_TOKEN=$(gh auth token)` fills it in, fetching only the newest pages (a few requests) unless it is far behind. - **`stars.csv`** holds only current stargazers. When someone unstars, their row disappears at the next resync, so the cumulative chart shows today's stargazers by the date they starred, not a historical peak. `daily.csv` records what the total actually was on each day. Logins are deliberately not stored: the charts only need timestamps, and there is no reason to publish a permanent, append-only list of who starred. - **Watchers** is `subscribers_count` (people watching the repo), not the legacy `watchers_count` alias for stars. - **Open issues** excludes pull requests. GitHub's `open_issues_count` includes PRs, so open PRs are counted separately and subtracted. diff --git a/charts/stars_growth.svg b/charts/stars_growth.svg index 5715c8f..46bd16c 100644 --- a/charts/stars_growth.svg +++ b/charts/stars_growth.svg @@ -40,73 +40,71 @@ L 0 4 " style="stroke: #c3c2b7; stroke-width: 0.8"/> - + - - + + - - - - - - + + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + - + - - - - - - - - - - - + + + + + + - - - - + + + + + + + + + + + - - - + + + + + + + + + - - - - + - - - - - + + + - - - + + + + - - - - - - + - - - - + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + - - - - - - - +" clip-path="url(#p60cb94f407)" style="fill: #9ec5f4"/> - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - + + + - - - - - - +" clip-path="url(#p60cb94f407)" style="fill: #9ec5f4"/> - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + + + + + + + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - - - + + + + - + - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + +" style="fill: none; stroke: #2a78d6; stroke-width: 1.5; stroke-linecap: round"/> - + @@ -4022,13 +12275,6 @@ L 2766 4134 L 525 4134 L 525 4666 z -" transform="scale(0.015625)"/> - - + + - + - + @@ -4137,19 +12409,6 @@ Q 3275 0 1925 0 L 628 0 L 628 4666 z -" transform="scale(0.015625)"/> - @@ -4161,7 +12420,7 @@ z - + @@ -4574,29 +12833,21 @@ z - - + + - - - + - - - - @@ -4876,49 +13087,45 @@ z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/collect.py b/collect.py index f10bfe8..3104b23 100644 --- a/collect.py +++ b/collect.py @@ -7,8 +7,9 @@ every source has been fetched and validated, so a failed run never leaves a partial or garbage row behind. -Needs no secrets beyond an optional GITHUB_TOKEN (or GH_TOKEN), which only -raises the GitHub API rate limit. +Needs no secrets beyond an optional GITHUB_TOKEN (or GH_TOKEN). In Actions the built-in +token raises the rate limit; per-star timestamps (stars.csv) are only refreshed with a +user token, e.g. GITHUB_TOKEN=$(gh auth token) locally. See TokenNotAccepted. """ from __future__ import annotations @@ -63,8 +64,10 @@ def log(msg: str) -> None: class TokenNotAccepted(RuntimeError): """401, or 403 "Resource not accessible by integration". - Listing stargazers with timestamps requires a user token: the workflow's - repo-scoped GITHUB_TOKEN gets a 403 and unauthenticated requests get a 401. + Listing stargazers requires a user token. Unauthenticated requests get a 401, and the + workflow's GITHUB_TOKEN gets a 403 "Resource not accessible by integration" from REST + (with or without the star+json media type) and from GraphQL's `stargazers` alike + (checked 2026-10-13). Reading another public repo's metadata works with it. """ @@ -78,11 +81,7 @@ def __init__(self) -> None: "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", } - token = ( - os.environ.get("STATS_GH_TOKEN") - or os.environ.get("GITHUB_TOKEN") - or os.environ.get("GH_TOKEN") - ) + token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") if token: self.gh_headers["Authorization"] = f"Bearer {token}" else: @@ -253,8 +252,8 @@ def collect_stars(http: Http, stored: list[str], total: int, force_full: bool) - try: return _collect_stars(http, stored, total, force_full) except TokenNotAccepted as e: - # Per-star timestamps need a user token (STATS_GH_TOKEN secret). Without one, keep - # the stored history; daily.csv still records the stargazers_count for the chart. + # Per-star timestamps need a user token. Without one, keep the stored history; + # daily.csv still records stargazers_count, which the charts continue from. log(f"warning: star timestamps unavailable, keeping stars.csv as is ({e})") return stored diff --git a/render.py b/render.py index 8594898..34feaed 100644 --- a/render.py +++ b/render.py @@ -21,7 +21,7 @@ matplotlib.use("Agg") import matplotlib.dates as mdates # noqa: E402 import matplotlib.pyplot as plt # noqa: E402 -from matplotlib.ticker import FuncFormatter, MaxNLocator # noqa: E402 +from matplotlib.ticker import FixedLocator, FuncFormatter, MaxNLocator # noqa: E402 ROOT = Path(__file__).resolve().parent DATA = ROOT / "data" @@ -29,7 +29,6 @@ README = ROOT / "README.md" START, END = "", "" MIN_DAYS_FOR_DOWNLOADS_CHART = 14 -STAR_GROWTH_DAYS = 365 # window of the star growth chart; the 2024 launch spike would flatten anything wider # Light chart card; reads fine embedded on both GitHub light and dark pages. SURFACE = "#fcfcfb" @@ -184,26 +183,47 @@ def render_star_growth(totals: dict[dt.date, int]) -> bool: if len(days) < 2: return False added = {d: totals[d] - totals[d - dt.timedelta(days=1)] for d in days[1:]} - end = days[-1] - start = max(days[1], end - dt.timedelta(days=STAR_GROWTH_DAYS - 1)) - shown = [d for d in sorted(added) if d >= start] - xs, ys = rolling_mean(sorted(added), added, window=7) - avg = [(x, y) for x, y in zip(xs, ys) if x >= start] - week = sum(added.get(end - dt.timedelta(days=i), 0) for i in range(7)) - prev = sum(added.get(end - dt.timedelta(days=i), 0) for i in range(7, 14)) + shown = sorted(added) + end = shown[-1] + xs, ys = rolling_mean(shown, added, window=7) + + def week_ending(d: dt.date) -> int: + return sum(added.get(d - dt.timedelta(days=i), 0) for i in range(7)) + + week, prev = week_ending(end), week_ending(end - dt.timedelta(days=7)) fig, ax = new_figure( "GitHub stars added per day (7-day rolling avg)", - f"Net new stargazers of gptme/gptme per day, last {STAR_GROWTH_DAYS} days. " - f"Last 7 days: {week:,}, previous 7: {prev:,}", + f"Net new stargazers of gptme/gptme per day, log scale. Last 7 days: {week:,}, previous 7: {prev:,}", ) - ax.bar(shown, [added[d] for d in shown], width=0.8, color=BLUE_LIGHT, linewidth=0, label="Daily") - if avg: - ax.plot([x for x, _ in avg], [y for _, y in avg], color=BLUE, linewidth=2, label="7-day average") - end_label(ax, avg[-1][0], avg[-1][1], f"{avg[-1][1]:,.1f}/day") + ax.bar(shown, [added[d] for d in shown], width=1, color=BLUE_LIGHT, linewidth=0, label="Daily") + if xs: + ax.plot(xs, ys, color=BLUE, linewidth=1.5, label="7-day average") + end_label(ax, xs[-1], ys[-1], f"{ys[-1]:,.1f}/day") + # Daily counts span 0 to a few hundred (the October 2024 launch). A log scale keeps + # a quiet week at 1/day readable next to that spike, and equal ratios get equal + # heights, so "twice last week" looks the same at any level. symlog is linear in + # [-1, 1] so zero and unstar days still have a place. + hi = max(added.values()) + ax.set_yscale("symlog", linthresh=1, linscale=0.4) + ticks = [0] + [v for v in (1, 3, 10, 30, 100, 300, 1000, 3000) if v <= hi * 1.5] + ax.yaxis.set_major_locator(FixedLocator(ticks)) + ax.yaxis.set_minor_locator(FixedLocator([])) + ax.yaxis.set_major_formatter(comma) + ax.set_ylim(min(-0.6, min(added.values()) * 1.05), max(2, hi) * 1.6) ax.axhline(0, color=BASELINE, linewidth=1, zorder=1) - ax.set_xlim(start - dt.timedelta(days=1), end + dt.timedelta(days=1)) - lo = min(0, min(added[d] for d in shown)) - ax.set_ylim(lo * 1.08, max(1, max(added[d] for d in shown)) * 1.08) + ax.set_xlim(shown[0], end + dt.timedelta(days=1)) + peak = max(shown, key=week_ending) + if week_ending(peak) >= 4 * max(1, week): + top = max(shown[max(0, shown.index(peak) - 6) : shown.index(peak) + 1], key=added.__getitem__) + ax.annotate( + f"busiest week: {week_ending(peak):,} stars (to {peak:%Y-%m-%d})", + (top, added[top]), + xytext=(8, 0), + textcoords="offset points", + va="center", + fontsize=8.5, + color=INK_2, + ) ax.legend(loc="upper left", fontsize=9, labelcolor=INK_2, handlelength=1.5) save(fig, "stars_growth.svg") return True From d8f87cf7fa4bb47a0c1ac985894a8fd1b0168df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 14 Oct 2026 01:21:43 +0200 Subject: [PATCH 6/6] perf(render): draw daily star counts as one step fill to keep the SVG small --- charts/stars_growth.svg | 15697 +++++++++++++------------------------- render.py | 5 +- 2 files changed, 5264 insertions(+), 10438 deletions(-) diff --git a/charts/stars_growth.svg b/charts/stars_growth.svg index 46bd16c..54cd301 100644 --- a/charts/stars_growth.svg +++ b/charts/stars_growth.svg @@ -471,10381 +471,5204 @@ L 495.36 68.075906 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - + @@ -12256,15 +7079,71 @@ z + + + + + + + + + + + + + + + + + - - + - + - - - - - - - - - - - - - - - - - diff --git a/render.py b/render.py index 34feaed..8a7b8f2 100644 --- a/render.py +++ b/render.py @@ -195,7 +195,10 @@ def week_ending(d: dt.date) -> int: "GitHub stars added per day (7-day rolling avg)", f"Net new stargazers of gptme/gptme per day, log scale. Last 7 days: {week:,}, previous 7: {prev:,}", ) - ax.bar(shown, [added[d] for d in shown], width=1, color=BLUE_LIGHT, linewidth=0, label="Daily") + # One filled step path instead of ~1,000 bar patches keeps the SVG (and daily diffs) small. + ax.fill_between( + shown, [added[d] for d in shown], step="mid", color=BLUE_LIGHT, linewidth=0, label="Daily" + ) if xs: ax.plot(xs, ys, color=BLUE, linewidth=1.5, label="7-day average") end_label(ax, xs[-1], ys[-1], f"{ys[-1]:,.1f}/day")