1- import { writeFileSync , mkdirSync } from 'fs' ;
1+ import { mkdirSync , writeFileSync } from 'node: fs' ;
22
33const repo = 'LotusInputMethod/fcitx5-lotus' ;
44const token = process . env . STAR_TOKEN ;
55
6- if ( ! repo || ! token ) {
7- throw new Error ( 'Missing GITHUB_REPOSITORY or STAR_TOKEN' ) ;
6+ if ( ! token ) {
7+ throw new Error ( 'Missing STAR_TOKEN' ) ;
88}
99
1010const stars = [ ] ;
1111let page = 1 ;
12+
1213while ( true ) {
1314 const res = await fetch (
1415 `https://api.github.com/repos/${ repo } /stargazers?per_page=100&page=${ page } ` ,
@@ -20,52 +21,287 @@ while (true) {
2021 } ,
2122 } ,
2223 ) ;
23- if ( ! res . ok ) throw new Error ( `GitHub API ${ res . status } : ${ await res . text ( ) } ` ) ;
24+
25+ if ( ! res . ok ) {
26+ throw new Error ( `GitHub API ${ res . status } : ${ await res . text ( ) } ` ) ;
27+ }
28+
2429 const data = await res . json ( ) ;
30+
2531 if ( data . length === 0 ) break ;
26- for ( const s of data ) stars . push ( new Date ( s . starred_at ) . getTime ( ) ) ;
32+
33+ for ( const star of data ) {
34+ if ( star . starred_at ) {
35+ stars . push ( new Date ( star . starred_at ) ) ;
36+ }
37+ }
38+
2739 if ( data . length < 100 ) break ;
28- page ++ ;
40+
41+ page += 1 ;
42+ }
43+
44+ if ( stars . length === 0 ) {
45+ throw new Error ( 'No stargazer data returned' ) ;
2946}
3047
3148stars . sort ( ( a , b ) => a - b ) ;
3249
33- const byDay = new Map ( ) ;
34- stars . forEach ( ( t , i ) => {
35- byDay . set ( new Date ( t ) . toISOString ( ) . slice ( 0 , 10 ) , i + 1 ) ;
50+ const formatDate = ( date ) => date . toISOString ( ) . slice ( 0 , 10 ) ;
51+
52+ const parseDate = ( dateString ) => {
53+ const [ year , month , day ] = dateString . split ( '-' ) . map ( Number ) ;
54+ return new Date ( Date . UTC ( year , month - 1 , day ) ) ;
55+ } ;
56+
57+ const addDays = ( date , days ) => {
58+ const result = new Date ( date ) ;
59+ result . setUTCDate ( result . getUTCDate ( ) + days ) ;
60+ return result ;
61+ } ;
62+
63+ const firstDay = parseDate ( formatDate ( stars [ 0 ] ) ) ;
64+ const lastDay = parseDate ( formatDate ( new Date ( ) ) ) ;
65+
66+ const starsByDay = new Map ( ) ;
67+
68+ for ( const star of stars ) {
69+ const day = formatDate ( star ) ;
70+ starsByDay . set ( day , ( starsByDay . get ( day ) ?? 0 ) + 1 ) ;
71+ }
72+
73+ const points = [ ] ;
74+ let count = 0 ;
75+ let currentDay = firstDay ;
76+
77+ while ( currentDay <= lastDay ) {
78+ const day = formatDate ( currentDay ) ;
79+
80+ count += starsByDay . get ( day ) ?? 0 ;
81+
82+ points . push ( {
83+ day,
84+ date : new Date ( currentDay ) ,
85+ count,
86+ } ) ;
87+
88+ currentDay = addDays ( currentDay , 1 ) ;
89+ }
90+
91+ const W = 1200 ;
92+ const H = 600 ;
93+
94+ const PAD_LEFT = 80 ;
95+ const PAD_RIGHT = 40 ;
96+ const PAD_TOP = 70 ;
97+ const PAD_BOTTOM = 85 ;
98+
99+ const CHART_W = W - PAD_LEFT - PAD_RIGHT ;
100+ const CHART_H = H - PAD_TOP - PAD_BOTTOM ;
101+
102+ const maxCount = Math . max ( ...points . map ( ( point ) => point . count ) , 1 ) ;
103+ const maxDate = points [ points . length - 1 ] . date . getTime ( ) ;
104+ const minDate = points [ 0 ] . date . getTime ( ) ;
105+ const dateRange = Math . max ( maxDate - minDate , 1 ) ;
106+
107+ const getX = ( point ) => {
108+ const progress = ( point . date . getTime ( ) - minDate ) / dateRange ;
109+ return PAD_LEFT + progress * CHART_W ;
110+ } ;
111+
112+ const getY = ( point ) => {
113+ const progress = point . count / maxCount ;
114+ return PAD_TOP + CHART_H - progress * CHART_H ;
115+ } ;
116+
117+ const linePoints = points
118+ . map ( ( point ) => `${ getX ( point ) . toFixed ( 2 ) } ,${ getY ( point ) . toFixed ( 2 ) } ` )
119+ . join ( ' ' ) ;
120+
121+ const areaPoints = [
122+ `${ PAD_LEFT } ,${ PAD_TOP + CHART_H } ` ,
123+ ...points . map (
124+ ( point ) => `${ getX ( point ) . toFixed ( 2 ) } ,${ getY ( point ) . toFixed ( 2 ) } ` ,
125+ ) ,
126+ `${ PAD_LEFT + CHART_W } ,${ PAD_TOP + CHART_H } ` ,
127+ ] . join ( ' ' ) ;
128+
129+ const escapeXml = ( value ) =>
130+ String ( value )
131+ . replaceAll ( '&' , '&' )
132+ . replaceAll ( '<' , '<' )
133+ . replaceAll ( '>' , '>' )
134+ . replaceAll ( '"' , '"' )
135+ . replaceAll ( "'" , ''' ) ;
136+
137+ const formatLabel = ( dateString ) => {
138+ const [ year , month , day ] = dateString . split ( '-' ) ;
139+ return `${ day } /${ month } /${ year } ` ;
140+ } ;
141+
142+ const tickCount = 8 ;
143+ const xTicks = Array . from ( { length : tickCount } , ( _ , index ) => {
144+ const progress = index / ( tickCount - 1 ) ;
145+ const pointIndex = Math . round ( progress * ( points . length - 1 ) ) ;
146+ const point = points [ pointIndex ] ;
147+
148+ return {
149+ x : getX ( point ) ,
150+ label : formatLabel ( point . day ) ,
151+ } ;
36152} ) ;
37- const points = [ ...byDay . entries ( ) ] . map ( ( [ day , count ] ) => ( {
38- day,
39- count,
40- } ) ) ;
41-
42- const W = 800 ,
43- H = 400 ,
44- PAD = 50 ;
45- const maxCount = Math . max ( ...points . map ( ( p ) => p . count ) , 1 ) ;
46- const n = points . length ;
47- const xy = points . map ( ( p , i ) => {
48- const x = PAD + ( i / Math . max ( n - 1 , 1 ) ) * ( W - 2 * PAD ) ;
49- const y = H - PAD - ( p . count / maxCount ) * ( H - 2 * PAD ) ;
50- return `${ x . toFixed ( 1 ) } ,${ y . toFixed ( 1 ) } ` ;
153+
154+ const yTickCount = 5 ;
155+ const yTicks = Array . from ( { length : yTickCount } , ( _ , index ) => {
156+ const progress = index / ( yTickCount - 1 ) ;
157+ const value = Math . round ( maxCount * progress ) ;
158+ const y = PAD_TOP + CHART_H - progress * CHART_H ;
159+
160+ return {
161+ value,
162+ y,
163+ } ;
51164} ) ;
52165
53- const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${ W } ${ H } " font-family="sans-serif">
54- <rect width="${ W } " height="${ H } " fill="#0d1117" rx="8"/>
55- <text x="${ W / 2 } " y="32" fill="#e6edf3" font-size="18" text-anchor="middle" font-weight="bold">
56- ${ repo } — Star History (${ maxCount } ⭐)
57- </text>
58- <line x1="${ PAD } " y1="${ H - PAD } " x2="${ W - PAD } " y2="${ H - PAD } " stroke="#30363d"/>
59- <line x1="${ PAD } " y1="${ PAD } " x2="${ PAD } " y2="${ H - PAD } " stroke="#30363d"/>
60- <text x="${ PAD - 8 } " y="${ PAD + 5 } " fill="#8b949e" font-size="12" text-anchor="end">${ maxCount } </text>
61- <text x="${ PAD - 8 } " y="${ H - PAD + 5 } " fill="#8b949e" font-size="12" text-anchor="end">0</text>
62- <text x="${ PAD } " y="${ H - PAD + 22 } " fill="#8b949e" font-size="12">${ points [ 0 ] ?. day ?? '' } </text>
63- <text x="${ W - PAD } " y="${ H - PAD + 22 } " fill="#8b949e" font-size="12" text-anchor="end">${ points [ n - 1 ] ?. day ?? '' } </text>
64- <polyline points="${ xy . join ( ' ' ) } " fill="none" stroke="#f0b429" stroke-width="2.5" stroke-linejoin="round"/>
65- </svg>` ;
166+ const horizontalGrid = yTicks
167+ . map (
168+ ( tick ) => `
169+ <line
170+ x1="${ PAD_LEFT } "
171+ y1="${ tick . y . toFixed ( 2 ) } "
172+ x2="${ PAD_LEFT + CHART_W } "
173+ y2="${ tick . y . toFixed ( 2 ) } "
174+ stroke="#30363d"
175+ stroke-width="1"
176+ />
177+ <text
178+ x="${ PAD_LEFT - 12 } "
179+ y="${ ( tick . y + 5 ) . toFixed ( 2 ) } "
180+ fill="#8b949e"
181+ font-size="13"
182+ text-anchor="end"
183+ >${ tick . value } </text>
184+ ` ,
185+ )
186+ . join ( '' ) ;
187+
188+ const verticalTicks = xTicks
189+ . map (
190+ ( tick ) => `
191+ <line
192+ x1="${ tick . x . toFixed ( 2 ) } "
193+ y1="${ PAD_TOP } "
194+ x2="${ tick . x . toFixed ( 2 ) } "
195+ y2="${ PAD_TOP + CHART_H } "
196+ stroke="#21262d"
197+ stroke-width="1"
198+ />
199+ <text
200+ x="${ tick . x . toFixed ( 2 ) } "
201+ y="${ PAD_TOP + CHART_H + 28 } "
202+ fill="#8b949e"
203+ font-size="13"
204+ text-anchor="middle"
205+ >${ tick . label } </text>
206+ ` ,
207+ )
208+ . join ( '' ) ;
209+
210+ const svg = `
211+ <svg
212+ xmlns="http://www.w3.org/2000/svg"
213+ viewBox="0 0 ${ W } ${ H } "
214+ role="img"
215+ aria-labelledby="title description"
216+ font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif"
217+ >
218+ <title id="title">GitHub Star History for ${ escapeXml ( repo ) } </title>
219+ <desc id="description">
220+ Star count increased to ${ maxCount } from ${ formatLabel ( points [ 0 ] . day ) }
221+ to ${ formatLabel ( points [ points . length - 1 ] . day ) } .
222+ </desc>
223+
224+ <rect
225+ width="${ W } "
226+ height="${ H } "
227+ rx="12"
228+ fill="#0d1117"
229+ />
230+
231+ <text
232+ x="${ W / 2 } "
233+ y="36"
234+ fill="#e6edf3"
235+ font-size="21"
236+ font-weight="600"
237+ text-anchor="middle"
238+ >${ escapeXml ( repo ) } — Star History</text>
239+
240+ <text
241+ x="${ W / 2 } "
242+ y="59"
243+ fill="#8b949e"
244+ font-size="13"
245+ text-anchor="middle"
246+ >${ maxCount . toLocaleString ( ) } stars</text>
247+
248+ ${ horizontalGrid }
249+ ${ verticalTicks }
250+
251+ <line
252+ x1="${ PAD_LEFT } "
253+ y1="${ PAD_TOP } "
254+ x2="${ PAD_LEFT } "
255+ y2="${ PAD_TOP + CHART_H } "
256+ stroke="#8b949e"
257+ stroke-width="1"
258+ />
259+
260+ <line
261+ x1="${ PAD_LEFT } "
262+ y1="${ PAD_TOP + CHART_H } "
263+ x2="${ PAD_LEFT + CHART_W } "
264+ y2="${ PAD_TOP + CHART_H } "
265+ stroke="#8b949e"
266+ stroke-width="1"
267+ />
268+
269+ <polygon
270+ points="${ areaPoints } "
271+ fill="#f0b429"
272+ fill-opacity="0.12"
273+ />
274+
275+ <polyline
276+ points="${ linePoints } "
277+ fill="none"
278+ stroke="#f0b429"
279+ stroke-width="3"
280+ stroke-linecap="round"
281+ stroke-linejoin="round"
282+ />
283+
284+ <circle
285+ cx="${ getX ( points [ points . length - 1 ] ) . toFixed ( 2 ) } "
286+ cy="${ getY ( points [ points . length - 1 ] ) . toFixed ( 2 ) } "
287+ r="5"
288+ fill="#f0b429"
289+ />
290+
291+ <text
292+ x="${ getX ( points [ points . length - 1 ] ) - 10 } "
293+ y="${ getY ( points [ points . length - 1 ] ) - 14 } "
294+ fill="#f0b429"
295+ font-size="14"
296+ font-weight="600"
297+ text-anchor="end"
298+ >${ maxCount . toLocaleString ( ) } </text>
299+ </svg>
300+ ` . trim ( ) ;
66301
67302mkdirSync ( 'public' , { recursive : true } ) ;
68303writeFileSync ( 'public/starcharts.svg' , svg ) ;
304+
69305console . log (
70- `Generated public/starcharts.svg with ${ maxCount } stars, ${ n } points ` ,
306+ `Generated public/starcharts.svg with ${ maxCount } stars across ${ points . length } days ` ,
71307) ;
0 commit comments