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) {