diff --git a/.github/scripts/star-history.mjs b/.github/scripts/star-history.mjs
index 834d054d..5006e685 100644
--- a/.github/scripts/star-history.mjs
+++ b/.github/scripts/star-history.mjs
@@ -5,6 +5,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
const DAY = 24 * 60 * 60 * 1000;
+const Y_AXIS_STEP = 5_000;
function mergePoint(points, point) {
return [...new Map([...points, point].map((item) => [item.date, item])).values()].sort(
@@ -28,10 +29,32 @@ function pointsFromStargazers(stargazers) {
.map(([date, count]) => ({ date, stars: (stars += count) }));
}
-function niceMaximum(value) {
- if (value <= 0) return 1;
- const magnitude = 10 ** Math.floor(Math.log10(value));
- return [1, 2, 5, 10].map((step) => step * magnitude).find((candidate) => candidate >= value);
+function yAxisMaximum(value) {
+ if (value <= 0) return Y_AXIS_STEP;
+ const rounded = Math.ceil(value / Y_AXIS_STEP) * Y_AXIS_STEP;
+ // Keep a small headroom only when value lands exactly on a tick.
+ return value % Y_AXIS_STEP === 0 ? rounded + Y_AXIS_STEP : rounded;
+}
+
+function xAxisTicks(minimumTime, maximumTime) {
+ const startYear = new Date(minimumTime).getUTCFullYear() - 1;
+ const endYear = new Date(maximumTime).getUTCFullYear() + 1;
+ const ticks = [];
+ for (let year = startYear; year <= endYear; year += 1) {
+ const major = Date.UTC(year, 1, 14);
+ const minor = Date.UTC(year, 7, 14);
+ if (major >= minimumTime && major <= maximumTime) ticks.push({ time: major, major: true });
+ if (minor >= minimumTime && minor <= maximumTime) ticks.push({ time: minor, major: false });
+ }
+ return ticks.sort((left, right) => left.time - right.time);
+}
+
+function formatUtcDate(time) {
+ const date = new Date(time);
+ const year = date.getUTCFullYear();
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
+ const day = String(date.getUTCDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
}
function escapeXml(value) {
@@ -57,7 +80,7 @@ function renderSvg(history) {
const minimumTime = Math.min(...times);
const maximumTime = Math.max(...times);
const timeSpan = Math.max(maximumTime - minimumTime, DAY);
- const maximumStars = niceMaximum(Math.max(...history.points.map(({ stars }) => stars)));
+ const maximumStars = yAxisMaximum(Math.max(...history.points.map(({ stars }) => stars)));
const x = (time) => history.points.length === 1
? width - right
: left + ((time - minimumTime) / timeSpan) * plotWidth;
@@ -72,22 +95,17 @@ function renderSvg(history) {
}
const area = `${line} V ${height - bottom} H ${coordinates[0].x.toFixed(1)} Z`;
const number = new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 });
- const date = new Intl.DateTimeFormat("en", {
- day: "numeric",
- month: "short",
- year: "numeric",
- timeZone: "UTC",
- });
- const grid = Array.from({ length: 6 }, (_, index) => {
- const stars = (maximumStars * index) / 5;
+ const grid = Array.from({ length: Math.floor(maximumStars / Y_AXIS_STEP) + 1 }, (_, index) => {
+ const stars = index * Y_AXIS_STEP;
const position = y(stars);
return `${number.format(stars)}`;
}).join("");
- const dates = Array.from({ length: 5 }, (_, index) => {
- const time = minimumTime + (timeSpan * index) / 4;
- const position = left + (plotWidth * index) / 4;
- const anchor = index === 0 ? "start" : index === 4 ? "end" : "middle";
- return `${date.format(time)}`;
+ const dates = xAxisTicks(minimumTime, maximumTime).map(({ time, major }) => {
+ const position = x(time);
+ if (major) {
+ return `${formatUtcDate(time)}`;
+ }
+ return ``;
}).join("");
const latest = history.points.at(-1);
@@ -98,6 +116,7 @@ function renderSvg(history) {